Files CRUD.
Remove Contacts and Calendar + CardDav and CalDav.
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
// Nextcloud/ownCloud mimicry
|
||||
export function handler(): Response {
|
||||
return new Response('Redirecting...', {
|
||||
status: 307,
|
||||
headers: { Location: '/dav' },
|
||||
});
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Nextcloud/ownCloud mimicry
|
||||
export function handler(): Response {
|
||||
return new Response('Redirecting...', {
|
||||
status: 307,
|
||||
headers: { Location: '/dav' },
|
||||
});
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { createCalendarEvent, getCalendar, getCalendarEvents } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarIds: string[];
|
||||
calendarView: 'day' | 'week' | 'month';
|
||||
calendarStartDate: string;
|
||||
calendarId: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isAllDay?: boolean;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendarEvents: CalendarEvent[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.calendarId || !requestBody.calendarIds || !requestBody.title || !requestBody.startDate ||
|
||||
!requestBody.endDate || !requestBody.calendarView || !requestBody.calendarStartDate
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const calendar = await getCalendar(requestBody.calendarId, context.state.user.id);
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const newCalendarEvent = await createCalendarEvent(
|
||||
context.state.user.id,
|
||||
requestBody.calendarId,
|
||||
requestBody.title,
|
||||
new Date(requestBody.startDate),
|
||||
new Date(requestBody.endDate),
|
||||
requestBody.isAllDay,
|
||||
);
|
||||
|
||||
if (!newCalendarEvent) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const dateRange = { start: new Date(requestBody.calendarStartDate), end: new Date(requestBody.calendarStartDate) };
|
||||
|
||||
if (requestBody.calendarView === 'day') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 1);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 1);
|
||||
} else if (requestBody.calendarView === 'week') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 7);
|
||||
} else {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 31);
|
||||
}
|
||||
|
||||
const newCalendarEvents = await getCalendarEvents(context.state.user.id, requestBody.calendarIds, dateRange);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendarEvents };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, FreshContextState } from '/lib/types.ts';
|
||||
import { createCalendar, getCalendars } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendars: Calendar[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.name) {
|
||||
const newCalendar = await createCalendar(context.state.user.id, requestBody.name);
|
||||
|
||||
if (!newCalendar) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
const newCalendars = await getCalendars(context.state.user.id);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendars };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { deleteCalendarEvent, getCalendar, getCalendarEvent, getCalendarEvents } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarIds: string[];
|
||||
calendarView: 'day' | 'week' | 'month';
|
||||
calendarStartDate: string;
|
||||
calendarId: string;
|
||||
calendarEventId: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendarEvents: CalendarEvent[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.calendarId || !requestBody.calendarIds || !requestBody.calendarEventId ||
|
||||
!requestBody.calendarEventId ||
|
||||
!requestBody.calendarView || !requestBody.calendarStartDate
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const calendar = await getCalendar(requestBody.calendarId, context.state.user.id);
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const calendarEvent = await getCalendarEvent(
|
||||
requestBody.calendarEventId,
|
||||
context.state.user.id,
|
||||
);
|
||||
|
||||
if (!calendarEvent || requestBody.calendarId !== calendarEvent.calendar_id) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
await deleteCalendarEvent(requestBody.calendarEventId, requestBody.calendarId, context.state.user.id);
|
||||
|
||||
const dateRange = { start: new Date(requestBody.calendarStartDate), end: new Date(requestBody.calendarStartDate) };
|
||||
|
||||
if (requestBody.calendarView === 'day') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 1);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 1);
|
||||
} else if (requestBody.calendarView === 'week') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 7);
|
||||
} else {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 31);
|
||||
}
|
||||
|
||||
const newCalendarEvents = await getCalendarEvents(context.state.user.id, requestBody.calendarIds, dateRange);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendarEvents };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, FreshContextState } from '/lib/types.ts';
|
||||
import { deleteCalendar, getCalendar, getCalendars } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarId: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendars: Calendar[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.calendarId) {
|
||||
const calendar = await getCalendar(requestBody.calendarId, context.state.user.id);
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
await deleteCalendar(requestBody.calendarId, context.state.user.id);
|
||||
}
|
||||
|
||||
const newCalendars = await getCalendars(context.state.user.id);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendars };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { getCalendarEvents } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarIds: string[];
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
calendarEvents: CalendarEvent[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (!requestBody.calendarIds) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const calendarEvents = await getCalendarEvents(
|
||||
context.state.user.id,
|
||||
requestBody.calendarIds,
|
||||
);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, calendarEvents };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { concurrentPromises } from '/lib/utils/misc.ts';
|
||||
import { createCalendarEvent, getCalendar, getCalendarEvents, updateCalendarEvent } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarIds: string[];
|
||||
calendarView: 'day' | 'week' | 'month';
|
||||
calendarStartDate: string;
|
||||
partialCalendarEvents: Partial<CalendarEvent>[];
|
||||
calendarId: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendarEvents: CalendarEvent[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.calendarId || !requestBody.calendarIds || !requestBody.partialCalendarEvents ||
|
||||
!requestBody.calendarView || !requestBody.calendarStartDate
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const calendar = await getCalendar(requestBody.calendarId, context.state.user.id);
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
if (requestBody.partialCalendarEvents.length === 0) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
await concurrentPromises(
|
||||
requestBody.partialCalendarEvents.map((partialCalendarEvent) => async () => {
|
||||
if (partialCalendarEvent.title && partialCalendarEvent.start_date && partialCalendarEvent.end_date) {
|
||||
const calendarEvent = await createCalendarEvent(
|
||||
context.state.user!.id,
|
||||
requestBody.calendarId,
|
||||
partialCalendarEvent.title,
|
||||
new Date(partialCalendarEvent.start_date),
|
||||
new Date(partialCalendarEvent.end_date),
|
||||
partialCalendarEvent.is_all_day,
|
||||
);
|
||||
|
||||
const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {});
|
||||
|
||||
if (parsedExtra !== '{}') {
|
||||
calendarEvent.extra = partialCalendarEvent.extra!;
|
||||
|
||||
if (
|
||||
calendarEvent.extra.is_recurring && calendarEvent.extra.recurring_sequence === 0 &&
|
||||
!calendarEvent.extra.recurring_id
|
||||
) {
|
||||
calendarEvent.extra.recurring_id = calendarEvent.id;
|
||||
}
|
||||
|
||||
await updateCalendarEvent(calendarEvent);
|
||||
}
|
||||
}
|
||||
}),
|
||||
5,
|
||||
);
|
||||
|
||||
const dateRange = { start: new Date(requestBody.calendarStartDate), end: new Date(requestBody.calendarStartDate) };
|
||||
|
||||
if (requestBody.calendarView === 'day') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 1);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 1);
|
||||
} else if (requestBody.calendarView === 'week') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 7);
|
||||
} else {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 31);
|
||||
}
|
||||
|
||||
const newCalendarEvents = await getCalendarEvents(context.state.user.id, requestBody.calendarIds, dateRange);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendarEvents };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { searchCalendarEvents } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
calendarIds: string[];
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
matchingCalendarEvents: CalendarEvent[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.calendarIds || !requestBody.searchTerm
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const matchingCalendarEvents = await searchCalendarEvents(
|
||||
requestBody.searchTerm,
|
||||
context.state.user.id,
|
||||
requestBody.calendarIds,
|
||||
);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, matchingCalendarEvents };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, FreshContextState } from '/lib/types.ts';
|
||||
import { getCalendar, getCalendars, updateCalendar } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
is_visible: boolean;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newCalendars: Calendar[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.id) {
|
||||
const calendar = await getCalendar(requestBody.id, context.state.user.id);
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
calendar.name = requestBody.name;
|
||||
calendar.color = requestBody.color;
|
||||
calendar.is_visible = requestBody.is_visible;
|
||||
|
||||
await updateCalendar(calendar);
|
||||
}
|
||||
|
||||
const newCalendars = await getCalendars(context.state.user.id);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, newCalendars };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { createContact, getContacts } from '/lib/data/contacts.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
contacts: Pick<Contact, 'id' | 'first_name' | 'last_name'>[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.firstName) {
|
||||
const contact = await createContact(context.state.user.id, requestBody.firstName, requestBody.lastName);
|
||||
|
||||
if (!contact) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
const contacts = await getContacts(context.state.user.id, requestBody.page - 1);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, contacts };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { deleteContact, getContact, getContacts } from '/lib/data/contacts.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
contactId: string;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
contacts: Pick<Contact, 'id' | 'first_name' | 'last_name'>[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.contactId) {
|
||||
const contact = await getContact(requestBody.contactId, context.state.user.id);
|
||||
|
||||
if (!contact) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
await deleteContact(requestBody.contactId, context.state.user.id);
|
||||
}
|
||||
|
||||
const contacts = await getContacts(context.state.user.id, requestBody.page - 1);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, contacts };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { getAllContacts } from '/lib/data/contacts.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
contacts: Contact[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const contacts = await getAllContacts(context.state.user.id);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, contacts };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { concurrentPromises } from '/lib/utils/misc.ts';
|
||||
import { createContact, getContacts, updateContact } from '/lib/data/contacts.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
partialContacts: Partial<Contact>[];
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
contacts: Pick<Contact, 'id' | 'first_name' | 'last_name'>[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (requestBody.partialContacts) {
|
||||
if (requestBody.partialContacts.length === 0) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
await concurrentPromises(
|
||||
requestBody.partialContacts.map((partialContact) => async () => {
|
||||
if (partialContact.first_name) {
|
||||
const contact = await createContact(
|
||||
context.state.user!.id,
|
||||
partialContact.first_name,
|
||||
partialContact.last_name || '',
|
||||
);
|
||||
|
||||
const parsedExtra = JSON.stringify(partialContact.extra || {});
|
||||
|
||||
if (parsedExtra !== '{}') {
|
||||
contact.extra = partialContact.extra!;
|
||||
|
||||
await updateContact(contact);
|
||||
}
|
||||
}
|
||||
}),
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
const contacts = await getContacts(context.state.user.id, requestBody.page - 1);
|
||||
|
||||
const responseBody: ResponseBody = { success: true, contacts };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
47
routes/api/files/create-directory.tsx
Normal file
47
routes/api/files/create-directory.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||
import { createDirectory, getDirectories } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newDirectories: Directory[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.name?.trim() || !requestBody.parentPath.startsWith('/') ||
|
||||
requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path and get the appropriate ownerUserId
|
||||
|
||||
const createdDirectory = await createDirectory(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
requestBody.name.trim(),
|
||||
);
|
||||
|
||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: createdDirectory, newDirectories };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
47
routes/api/files/delete-directory.tsx
Normal file
47
routes/api/files/delete-directory.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||
import { deleteDirectoryOrFile, getDirectories } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newDirectories: Directory[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.name?.trim() || !requestBody.parentPath.startsWith('/') ||
|
||||
requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path and get the appropriate ownerUserId
|
||||
|
||||
const deletedDirectory = await deleteDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
requestBody.name.trim(),
|
||||
);
|
||||
|
||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: deletedDirectory, newDirectories };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
47
routes/api/files/delete.tsx
Normal file
47
routes/api/files/delete.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||
import { deleteDirectoryOrFile, getFiles } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newFiles: DirectoryFile[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.name?.trim() || !requestBody.parentPath.startsWith('/') ||
|
||||
requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path/file and get the appropriate ownerUserId
|
||||
|
||||
const deletedFile = await deleteDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
requestBody.name.trim(),
|
||||
);
|
||||
|
||||
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: deletedFile, newFiles };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
47
routes/api/files/get-directories.tsx
Normal file
47
routes/api/files/get-directories.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||
import { getDirectories } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
directoryPathToExclude?: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
directories: Directory[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.parentPath.startsWith('/') || requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const directories = await getDirectories(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
);
|
||||
|
||||
const filteredDirectories = requestBody.directoryPathToExclude
|
||||
? directories.filter((directory) =>
|
||||
`${directory.parent_path}${directory.directory_name}` !== requestBody.directoryPathToExclude
|
||||
)
|
||||
: directories;
|
||||
|
||||
const responseBody: ResponseBody = { success: true, directories: filteredDirectories };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
52
routes/api/files/move-directory.tsx
Normal file
52
routes/api/files/move-directory.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
oldParentPath: string;
|
||||
newParentPath: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newDirectories: Directory[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.oldParentPath || !requestBody.newParentPath || !requestBody.name?.trim() ||
|
||||
!requestBody.oldParentPath.startsWith('/') ||
|
||||
requestBody.oldParentPath.includes('../') || !requestBody.newParentPath.startsWith('/') ||
|
||||
requestBody.newParentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to old and new paths and get the appropriate ownerUserIds
|
||||
|
||||
const movedDirectory = await renameDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.oldParentPath,
|
||||
requestBody.newParentPath,
|
||||
requestBody.name.trim(),
|
||||
requestBody.name.trim(),
|
||||
);
|
||||
|
||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.oldParentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
52
routes/api/files/move.tsx
Normal file
52
routes/api/files/move.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
oldParentPath: string;
|
||||
newParentPath: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newFiles: DirectoryFile[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.oldParentPath || !requestBody.newParentPath || !requestBody.name?.trim() ||
|
||||
!requestBody.oldParentPath.startsWith('/') ||
|
||||
requestBody.oldParentPath.includes('../') || !requestBody.newParentPath.startsWith('/') ||
|
||||
requestBody.newParentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to old and new paths/files and get the appropriate ownerUserIds
|
||||
|
||||
const movedFile = await renameDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.oldParentPath,
|
||||
requestBody.newParentPath,
|
||||
requestBody.name.trim(),
|
||||
requestBody.name.trim(),
|
||||
);
|
||||
|
||||
const newFiles = await getFiles(context.state.user.id, requestBody.oldParentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
51
routes/api/files/rename-directory.tsx
Normal file
51
routes/api/files/rename-directory.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
oldName: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newDirectories: Directory[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.oldName?.trim() || !requestBody.newName?.trim() ||
|
||||
!requestBody.parentPath.startsWith('/') ||
|
||||
requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path and get the appropriate ownerUserId
|
||||
|
||||
const movedDirectory = await renameDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
requestBody.parentPath,
|
||||
requestBody.oldName.trim(),
|
||||
requestBody.newName.trim(),
|
||||
);
|
||||
|
||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: movedDirectory, newDirectories };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
51
routes/api/files/rename.tsx
Normal file
51
routes/api/files/rename.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface RequestBody {
|
||||
parentPath: string;
|
||||
oldName: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newFiles: DirectoryFile[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().json() as RequestBody;
|
||||
|
||||
if (
|
||||
!requestBody.parentPath || !requestBody.oldName?.trim() || !requestBody.newName?.trim() ||
|
||||
!requestBody.parentPath.startsWith('/') ||
|
||||
requestBody.parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path/file and get the appropriate ownerUserId
|
||||
|
||||
const movedFile = await renameDirectoryOrFile(
|
||||
context.state.user.id,
|
||||
requestBody.parentPath,
|
||||
requestBody.parentPath,
|
||||
requestBody.oldName.trim(),
|
||||
requestBody.newName.trim(),
|
||||
);
|
||||
|
||||
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: movedFile, newFiles };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
42
routes/api/files/upload.tsx
Normal file
42
routes/api/files/upload.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||
import { createFile, getFiles } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export interface ResponseBody {
|
||||
success: boolean;
|
||||
newFiles: DirectoryFile[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().formData();
|
||||
|
||||
const parentPath = requestBody.get('parent_path') as string;
|
||||
const name = requestBody.get('name') as string;
|
||||
const contents = requestBody.get('contents') as File;
|
||||
|
||||
if (
|
||||
!parentPath || !name.trim() || !contents || !parentPath.startsWith('/') ||
|
||||
parentPath.includes('../')
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Verify user has write access to path and get the appropriate ownerUserId
|
||||
|
||||
const createdFile = await createFile(context.state.user.id, parentPath, name.trim(), await contents.arrayBuffer());
|
||||
|
||||
const newFiles = await getFiles(context.state.user.id, parentPath);
|
||||
|
||||
const responseBody: ResponseBody = { success: createdFile, newFiles };
|
||||
|
||||
return new Response(JSON.stringify(responseBody));
|
||||
},
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { getCalendarEvents, getCalendars } from '/lib/data/calendar.ts';
|
||||
import CalendarWrapper from '/islands/calendar/CalendarWrapper.tsx';
|
||||
|
||||
interface Data {
|
||||
userCalendars: Pick<Calendar, 'id' | 'name' | 'color' | 'is_visible' | 'extra'>[];
|
||||
userCalendarEvents: CalendarEvent[];
|
||||
view: 'day' | 'week' | 'month';
|
||||
startDate: string;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
|
||||
const view = (searchParams.get('view') as Data['view']) || 'week';
|
||||
const startDate = searchParams.get('startDate') || new Date().toISOString().substring(0, 10);
|
||||
|
||||
const userCalendars = await getCalendars(context.state.user.id);
|
||||
const visibleCalendarIds = userCalendars.filter((calendar) => calendar.is_visible).map((calendar) => calendar.id);
|
||||
|
||||
const dateRange = { start: new Date(startDate), end: new Date(startDate) };
|
||||
|
||||
if (view === 'day') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 1);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 1);
|
||||
} else if (view === 'week') {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 7);
|
||||
} else {
|
||||
dateRange.start.setDate(dateRange.start.getDate() - 7);
|
||||
dateRange.end.setDate(dateRange.end.getDate() + 31);
|
||||
}
|
||||
|
||||
const userCalendarEvents = await getCalendarEvents(context.state.user.id, visibleCalendarIds, dateRange);
|
||||
|
||||
return await context.render({ userCalendars, userCalendarEvents, view, startDate });
|
||||
},
|
||||
};
|
||||
|
||||
export default function CalendarPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<CalendarWrapper
|
||||
initialCalendars={data.userCalendars}
|
||||
initialCalendarEvents={data.userCalendarEvents}
|
||||
view={data.view}
|
||||
startDate={data.startDate}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { convertFormDataToObject } from '/lib/utils/misc.ts';
|
||||
import { getCalendarEvent, getCalendars, updateCalendarEvent } from '/lib/data/calendar.ts';
|
||||
import { getFormDataField } from '/lib/form-utils.tsx';
|
||||
import ViewCalendarEvent, { formFields } from '/islands/calendar/ViewCalendarEvent.tsx';
|
||||
|
||||
interface Data {
|
||||
calendarEvent: CalendarEvent;
|
||||
calendars: Calendar[];
|
||||
error?: string;
|
||||
notice?: string;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const { calendarEventId } = context.params;
|
||||
|
||||
const calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
|
||||
|
||||
if (!calendarEvent) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const calendars = await getCalendars(context.state.user.id);
|
||||
|
||||
return await context.render({ calendarEvent, calendars, formData: {} });
|
||||
},
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const { calendarEventId } = context.params;
|
||||
|
||||
const calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
|
||||
|
||||
if (!calendarEvent) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const calendars = await getCalendars(context.state.user.id);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
calendarEvent.title = getFormDataField(formData, 'title');
|
||||
calendarEvent.start_date = new Date(getFormDataField(formData, 'start_date'));
|
||||
calendarEvent.end_date = new Date(getFormDataField(formData, 'end_date'));
|
||||
calendarEvent.is_all_day = getFormDataField(formData, 'is_all_day') === 'true';
|
||||
calendarEvent.status = getFormDataField(formData, 'status') as CalendarEvent['status'];
|
||||
|
||||
calendarEvent.extra.description = getFormDataField(formData, 'description') || undefined;
|
||||
calendarEvent.extra.url = getFormDataField(formData, 'url') || undefined;
|
||||
calendarEvent.extra.location = getFormDataField(formData, 'location') || undefined;
|
||||
calendarEvent.extra.transparency =
|
||||
getFormDataField(formData, 'transparency') as CalendarEvent['extra']['transparency'] || 'default';
|
||||
|
||||
const newCalendarId = getFormDataField(formData, 'calendar_id');
|
||||
let oldCalendarId: string | undefined;
|
||||
|
||||
if (newCalendarId !== calendarEvent.calendar_id) {
|
||||
oldCalendarId = calendarEvent.calendar_id;
|
||||
}
|
||||
|
||||
calendarEvent.calendar_id = newCalendarId;
|
||||
|
||||
try {
|
||||
if (!calendarEvent.title) {
|
||||
throw new Error(`Title is required.`);
|
||||
}
|
||||
|
||||
formFields(calendarEvent, calendars).forEach((field) => {
|
||||
if (field.required) {
|
||||
const value = formData.get(field.name);
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`${field.label} is required`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await updateCalendarEvent(calendarEvent, oldCalendarId);
|
||||
|
||||
return await context.render({
|
||||
calendarEvent,
|
||||
calendars,
|
||||
notice: 'Event updated successfully!',
|
||||
formData: convertFormDataToObject(formData),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return await context.render({
|
||||
calendarEvent,
|
||||
calendars,
|
||||
error: error.toString(),
|
||||
formData: convertFormDataToObject(formData),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default function ContactsPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<ViewCalendarEvent
|
||||
initialCalendarEvent={data.calendarEvent}
|
||||
calendars={data.calendars}
|
||||
formData={data.formData}
|
||||
error={data.error}
|
||||
notice={data.notice}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, FreshContextState } from '/lib/types.ts';
|
||||
import { getCalendars } from '/lib/data/calendar.ts';
|
||||
import Calendars from '/islands/calendar/Calendars.tsx';
|
||||
|
||||
interface Data {
|
||||
userCalendars: Calendar[];
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const userCalendars = await getCalendars(context.state.user.id);
|
||||
|
||||
return await context.render({ userCalendars });
|
||||
},
|
||||
};
|
||||
|
||||
export default function CalendarsPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<Calendars initialCalendars={data.userCalendars || []} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { getContacts, getContactsCount, searchContacts, searchContactsCount } from '/lib/data/contacts.ts';
|
||||
import Contacts from '/islands/contacts/Contacts.tsx';
|
||||
|
||||
interface Data {
|
||||
userContacts: Pick<Contact, 'id' | 'first_name' | 'last_name'>[];
|
||||
page: number;
|
||||
contactsCount: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const search = searchParams.get('search') || undefined;
|
||||
|
||||
const userContacts = search
|
||||
? await searchContacts(search, context.state.user.id, page - 1)
|
||||
: await getContacts(context.state.user.id, page - 1);
|
||||
|
||||
const contactsCount = search
|
||||
? await searchContactsCount(search, context.state.user.id)
|
||||
: await getContactsCount(context.state.user.id);
|
||||
|
||||
return await context.render({ userContacts, page, contactsCount, search });
|
||||
},
|
||||
};
|
||||
|
||||
export default function ContactsPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<Contacts
|
||||
initialContacts={data?.userContacts || []}
|
||||
page={data?.page || 1}
|
||||
contactsCount={data?.contactsCount || 0}
|
||||
search={data?.search || ''}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Contact, ContactAddress, ContactField, FreshContextState } from '/lib/types.ts';
|
||||
import { convertFormDataToObject } from '/lib/utils/misc.ts';
|
||||
import { getContact, updateContact } from '/lib/data/contacts.ts';
|
||||
import { getFormDataField, getFormDataFieldArray } from '/lib/form-utils.tsx';
|
||||
import ViewContact, { formFields } from '/islands/contacts/ViewContact.tsx';
|
||||
|
||||
interface Data {
|
||||
contact: Contact;
|
||||
error?: string;
|
||||
notice?: string;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const { contactId } = context.params;
|
||||
|
||||
const contact = await getContact(contactId, context.state.user.id);
|
||||
|
||||
if (!contact) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
return await context.render({ contact, formData: {} });
|
||||
},
|
||||
async POST(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const { contactId } = context.params;
|
||||
|
||||
const contact = await getContact(contactId, context.state.user.id);
|
||||
|
||||
if (!contact) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
contact.extra.name_title = getFormDataField(formData, 'name_title') || undefined;
|
||||
contact.first_name = getFormDataField(formData, 'first_name');
|
||||
contact.last_name = getFormDataField(formData, 'last_name');
|
||||
contact.extra.middle_names = getFormDataField(formData, 'middle_names').split(' ').map((name) =>
|
||||
(name || '').trim()
|
||||
).filter(Boolean);
|
||||
if (contact.extra.middle_names.length === 0) {
|
||||
contact.extra.middle_names = undefined;
|
||||
}
|
||||
contact.extra.birthday = getFormDataField(formData, 'birthday') || undefined;
|
||||
contact.extra.nickname = getFormDataField(formData, 'nickname') || undefined;
|
||||
contact.extra.organization = getFormDataField(formData, 'organization') || undefined;
|
||||
contact.extra.role = getFormDataField(formData, 'role') || undefined;
|
||||
contact.extra.photo_url = getFormDataField(formData, 'photo_url') || undefined;
|
||||
contact.extra.photo_mediatype = contact.extra.photo_url
|
||||
? `image/${contact.extra.photo_url.split('.').slice(-1, 1).join('').toLowerCase()}`
|
||||
: undefined;
|
||||
contact.extra.notes = getFormDataField(formData, 'notes') || undefined;
|
||||
|
||||
contact.extra.fields = [];
|
||||
|
||||
// Phones
|
||||
const phoneNumbers = getFormDataFieldArray(formData, 'phone_numbers');
|
||||
const phoneLabels = getFormDataFieldArray(formData, 'phone_labels');
|
||||
|
||||
for (const [index, phoneNumber] of phoneNumbers.entries()) {
|
||||
if (phoneNumber.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field: ContactField = {
|
||||
name: phoneLabels[index] || 'Home',
|
||||
value: phoneNumber.trim(),
|
||||
type: 'phone',
|
||||
};
|
||||
|
||||
contact.extra.fields.push(field);
|
||||
}
|
||||
|
||||
// Emails
|
||||
const emailAddresses = getFormDataFieldArray(formData, 'email_addresses');
|
||||
const emailLabels = getFormDataFieldArray(formData, 'email_labels');
|
||||
|
||||
for (const [index, emailAddress] of emailAddresses.entries()) {
|
||||
if (emailAddress.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field: ContactField = {
|
||||
name: emailLabels[index] || 'Home',
|
||||
value: emailAddress.trim(),
|
||||
type: 'email',
|
||||
};
|
||||
|
||||
contact.extra.fields.push(field);
|
||||
}
|
||||
|
||||
// URLs
|
||||
const urlAddresses = getFormDataFieldArray(formData, 'url_addresses');
|
||||
const urlLabels = getFormDataFieldArray(formData, 'url_labels');
|
||||
|
||||
for (const [index, urlAddress] of urlAddresses.entries()) {
|
||||
if (urlAddress.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field: ContactField = {
|
||||
name: urlLabels[index] || 'Home',
|
||||
value: urlAddress.trim(),
|
||||
type: 'url',
|
||||
};
|
||||
|
||||
contact.extra.fields.push(field);
|
||||
}
|
||||
|
||||
// Others
|
||||
const otherValues = getFormDataFieldArray(formData, 'other_values');
|
||||
const otherLabels = getFormDataFieldArray(formData, 'other_labels');
|
||||
|
||||
for (const [index, otherValue] of otherValues.entries()) {
|
||||
if (otherValue.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field: ContactField = {
|
||||
name: otherLabels[index] || 'Home',
|
||||
value: otherValue.trim(),
|
||||
type: 'other',
|
||||
};
|
||||
|
||||
contact.extra.fields.push(field);
|
||||
}
|
||||
|
||||
contact.extra.addresses = [];
|
||||
|
||||
// Addresses
|
||||
const addressLine1s = getFormDataFieldArray(formData, 'address_line_1s');
|
||||
const addressLine2s = getFormDataFieldArray(formData, 'address_line_2s');
|
||||
const addressCities = getFormDataFieldArray(formData, 'address_cities');
|
||||
const addressPostalCodes = getFormDataFieldArray(formData, 'address_postal_codes');
|
||||
const addressStates = getFormDataFieldArray(formData, 'address_states');
|
||||
const addressCountries = getFormDataFieldArray(formData, 'address_countries');
|
||||
const addressLabels = getFormDataFieldArray(formData, 'address_labels');
|
||||
|
||||
for (const [index, addressLine1] of addressLine1s.entries()) {
|
||||
if (addressLine1.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const address: ContactAddress = {
|
||||
label: addressLabels[index] || 'Home',
|
||||
line_1: addressLine1.trim(),
|
||||
line_2: addressLine2s[index] || undefined,
|
||||
city: addressCities[index] || undefined,
|
||||
postal_code: addressPostalCodes[index] || undefined,
|
||||
state: addressStates[index] || undefined,
|
||||
country: addressCountries[index] || undefined,
|
||||
};
|
||||
|
||||
contact.extra.addresses.push(address);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!contact.first_name) {
|
||||
throw new Error(`First name is required.`);
|
||||
}
|
||||
|
||||
formFields(contact).forEach((field) => {
|
||||
if (field.required) {
|
||||
const value = formData.get(field.name);
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`${field.label} is required`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await updateContact(contact);
|
||||
|
||||
return await context.render({
|
||||
contact,
|
||||
notice: 'Contact updated successfully!',
|
||||
formData: convertFormDataToObject(formData),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return await context.render({ contact, error: error.toString(), formData: convertFormDataToObject(formData) });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default function ContactsPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<ViewContact initialContact={data.contact} formData={data.formData} error={data.error} notice={data.notice} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml } from '/lib/utils/misc.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype'?: {
|
||||
'd:collection': {};
|
||||
'card:addressbook'?: {};
|
||||
};
|
||||
'd:displayname'?: string;
|
||||
'd:getetag'?: string;
|
||||
'd:current-user-principal'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'd:principal-URL'?: {};
|
||||
'card:addressbook-home-set'?: {};
|
||||
};
|
||||
'd:status': string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:card': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND' && request.method !== 'REPORT') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [
|
||||
{
|
||||
'd:href': '/dav/addressbooks/',
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': { 'd:principal-URL': {}, 'card:addressbook-home-set': {} },
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
},
|
||||
{
|
||||
'd:href': '/dav/addressbooks/contacts/',
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
'card:addressbook': {},
|
||||
},
|
||||
'd:displayname': 'Contacts',
|
||||
'd:getetag': escapeHtml(`"${context.state.user.extra.contacts_revision || 'new'}"`),
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': { 'd:principal-URL': {}, 'card:addressbook-home-set': {} },
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:card': 'urn:ietf:params:xml:ns:carddav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,243 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
import { parse } from 'xml/mod.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts';
|
||||
import { formatContactToVCard } from '/lib/utils/contacts.ts';
|
||||
import { getAllContacts } from '/lib/data/contacts.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype'?: {
|
||||
'd:collection'?: {};
|
||||
'card:addressbook'?: {};
|
||||
};
|
||||
'd:displayname'?: string | {};
|
||||
'card:address-data'?: string;
|
||||
'd:getlastmodified'?: string | {};
|
||||
'd:getetag'?: string | {};
|
||||
'd:getcontenttype'?: string | {};
|
||||
'd:getcontentlength'?: number | {};
|
||||
'd:creationdate'?: string | {};
|
||||
'card:addressbook-description'?: string | {};
|
||||
'cs:getctag'?: {};
|
||||
'd:current-user-privilege-set'?: {
|
||||
'd:privilege': {
|
||||
'd:write-properties'?: {};
|
||||
'd:write'?: {};
|
||||
'd:write-content'?: {};
|
||||
'd:unlock'?: {};
|
||||
'd:bind'?: {};
|
||||
'd:unbind'?: {};
|
||||
'd:write-acl'?: {};
|
||||
'd:read'?: {};
|
||||
'd:read-acl'?: {};
|
||||
'd:read-current-user-privilege-set'?: {};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
'd:status': string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:card': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
'xmlns:cs': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND' && request.method !== 'REPORT') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const contacts = await getAllContacts(context.state.user.id);
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
let includeVCard = false;
|
||||
let includeCollection = true;
|
||||
const includePrivileges = requestBody.includes('current-user-privilege-set');
|
||||
|
||||
const filterContactIds = new Set<string>();
|
||||
|
||||
try {
|
||||
const parsedDocument = parse(requestBody);
|
||||
|
||||
const multiGetRequest = (parsedDocument['addressbook-multiget'] || parsedDocument['r:addressbook-multiget'] ||
|
||||
parsedDocument['f:addressbook-multiget'] || parsedDocument['d:addressbook-multiget'] ||
|
||||
parsedDocument['r:addressbook-query'] ||
|
||||
parsedDocument['card:addressbook-multiget'] || parsedDocument['card:addressbook-query']) as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
|
||||
includeVCard = Boolean(multiGetRequest);
|
||||
|
||||
const requestedHrefs: string[] = (multiGetRequest && (multiGetRequest['href'] || multiGetRequest['d:href'])) || [];
|
||||
|
||||
includeCollection = requestedHrefs.length === 0;
|
||||
|
||||
for (const requestedHref of requestedHrefs) {
|
||||
const userVCard = requestedHref.split('/').slice(-1).join('');
|
||||
const [userId] = userVCard.split('.vcf');
|
||||
|
||||
if (userId) {
|
||||
filterContactIds.add(userId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse XML`, error);
|
||||
}
|
||||
|
||||
const filteredContacts = filterContactIds.size > 0
|
||||
? contacts.filter((contact) => filterContactIds.has(contact.id))
|
||||
: contacts;
|
||||
|
||||
const parsedContacts = filteredContacts.map((contact) => {
|
||||
const parsedContact: DavResponse = {
|
||||
'd:href': `/dav/addressbooks/contacts/${contact.id}.vcf`,
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:getlastmodified': buildRFC822Date(contact.updated_at.toISOString()),
|
||||
'd:getetag': escapeHtml(`"${contact.revision}"`),
|
||||
'd:getcontenttype': 'text/vcard; charset=utf-8',
|
||||
'd:resourcetype': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': {
|
||||
'd:displayname': {},
|
||||
'd:getcontentlength': {},
|
||||
'd:creationdate': {},
|
||||
'card:addressbook-description': {},
|
||||
'cs:getctag': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
};
|
||||
|
||||
if (includeVCard) {
|
||||
parsedContact['d:propstat'][0]['d:prop']['card:address-data'] = escapeXml(formatContactToVCard([contact]));
|
||||
}
|
||||
|
||||
if (includePrivileges) {
|
||||
parsedContact['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = {
|
||||
'd:privilege': [
|
||||
{ 'd:write-properties': {} },
|
||||
{ 'd:write': {} },
|
||||
{ 'd:write-content': {} },
|
||||
{ 'd:unlock': {} },
|
||||
{ 'd:bind': {} },
|
||||
{ 'd:unbind': {} },
|
||||
{ 'd:write-acl': {} },
|
||||
{ 'd:read': {} },
|
||||
{ 'd:read-acl': {} },
|
||||
{ 'd:read-current-user-privilege-set': {} },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return parsedContact;
|
||||
});
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [
|
||||
...parsedContacts,
|
||||
],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:card': 'urn:ietf:params:xml:ns:carddav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
'xmlns:cs': 'http://calendarserver.org/ns/',
|
||||
},
|
||||
};
|
||||
|
||||
if (includeCollection) {
|
||||
const collectionResponse: DavResponse = {
|
||||
'd:href': '/dav/addressbooks/contacts/',
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
'card:addressbook': {},
|
||||
},
|
||||
'd:displayname': 'Contacts',
|
||||
'd:getetag': escapeHtml(`"${context.state.user.extra.contacts_revision || 'new'}"`),
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': {
|
||||
'd:getlastmodified': {},
|
||||
'd:getcontenttype': {},
|
||||
'd:getcontentlength': {},
|
||||
'd:creationdate': {},
|
||||
'card:addressbook-description': {},
|
||||
'cs:getctag': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
};
|
||||
|
||||
if (includePrivileges) {
|
||||
collectionResponse['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = {
|
||||
'd:privilege': [
|
||||
{ 'd:write-properties': {} },
|
||||
{ 'd:write': {} },
|
||||
{ 'd:write-content': {} },
|
||||
{ 'd:unlock': {} },
|
||||
{ 'd:bind': {} },
|
||||
{ 'd:unbind': {} },
|
||||
{ 'd:write-acl': {} },
|
||||
{ 'd:read': {} },
|
||||
{ 'd:read-acl': {} },
|
||||
{ 'd:read-current-user-privilege-set': {} },
|
||||
],
|
||||
};
|
||||
}
|
||||
responseBody['d:multistatus']['d:response'].unshift(collectionResponse);
|
||||
}
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
import { parse } from 'xml/mod.ts';
|
||||
|
||||
import { Contact, FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts';
|
||||
import { formatContactToVCard, parseVCardFromTextContents } from '/lib/utils/contacts.ts';
|
||||
import { createContact, deleteContact, getContact, updateContact } from '/lib/data/contacts.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection'?: {};
|
||||
'card:addressbook'?: {};
|
||||
};
|
||||
'card:address-data'?: string;
|
||||
'd:getlastmodified'?: string;
|
||||
'd:getetag'?: string;
|
||||
'd:getcontenttype'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:card': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
request.method !== 'PROPFIND' && request.method !== 'REPORT' && request.method !== 'GET' &&
|
||||
request.method !== 'PUT' && request.method !== 'DELETE'
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const { contactId } = context.params;
|
||||
|
||||
let contact: Contact | null = null;
|
||||
|
||||
try {
|
||||
contact = await getContact(contactId, context.state.user.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
if (request.method === 'PUT') {
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
const [partialContact] = parseVCardFromTextContents(requestBody);
|
||||
|
||||
if (partialContact.first_name) {
|
||||
const newContact = await createContact(
|
||||
context.state.user.id,
|
||||
partialContact.first_name,
|
||||
partialContact.last_name || '',
|
||||
);
|
||||
|
||||
// Use the sent id for the UID
|
||||
if (!partialContact.extra?.uid) {
|
||||
partialContact.extra = {
|
||||
...(partialContact.extra || {}),
|
||||
uid: contactId,
|
||||
};
|
||||
}
|
||||
|
||||
newContact.extra = partialContact.extra!;
|
||||
|
||||
await updateContact(newContact);
|
||||
|
||||
contact = await getContact(newContact.id, context.state.user.id);
|
||||
|
||||
return new Response('Created', { status: 201, headers: { 'etag': `"${contact.revision}"` } });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (request.method === 'DELETE') {
|
||||
const clientRevision = request.headers.get('if-match') || request.headers.get('etag');
|
||||
|
||||
// Don't update outdated data
|
||||
if (clientRevision && clientRevision !== `"${contact.revision}"`) {
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${contact.revision}"` } });
|
||||
}
|
||||
|
||||
await deleteContact(contactId, context.state.user.id);
|
||||
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
if (request.method === 'PUT') {
|
||||
const clientRevision = request.headers.get('if-match') || request.headers.get('etag');
|
||||
|
||||
// Don't update outdated data
|
||||
if (clientRevision && clientRevision !== `"${contact.revision}"`) {
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${contact.revision}"` } });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
const [partialContact] = parseVCardFromTextContents(requestBody);
|
||||
|
||||
contact = {
|
||||
...contact,
|
||||
...partialContact,
|
||||
};
|
||||
|
||||
await updateContact(contact);
|
||||
|
||||
contact = await getContact(contactId, context.state.user.id);
|
||||
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${contact.revision}"` } });
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
// Set a UID if there isn't one
|
||||
if (!contact.extra.uid) {
|
||||
contact.extra.uid = crypto.randomUUID();
|
||||
await updateContact(contact);
|
||||
|
||||
contact = await getContact(contactId, context.state.user.id);
|
||||
}
|
||||
|
||||
const response = new Response(formatContactToVCard([contact]), {
|
||||
status: 200,
|
||||
headers: { 'etag': `"${contact.revision}"` },
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
}
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
let includeVCard = false;
|
||||
|
||||
try {
|
||||
const parsedDocument = parse(requestBody);
|
||||
|
||||
const multiGetRequest = (parsedDocument['r:addressbook-multiget'] || parsedDocument['r:addressbook-query'] ||
|
||||
parsedDocument['card:addressbook-multiget'] || parsedDocument['card:addressbook-query']) as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
|
||||
includeVCard = Boolean(multiGetRequest);
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse XML`, error);
|
||||
}
|
||||
|
||||
const parsedContact: DavResponse = {
|
||||
'd:href': `/dav/addressbooks/contacts/${contact.id}.vcf`,
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:getlastmodified': buildRFC822Date(contact.updated_at.toISOString()),
|
||||
'd:getetag': escapeHtml(`"${contact.revision}"`),
|
||||
'd:getcontenttype': 'text/vcard; charset=utf-8',
|
||||
'd:resourcetype': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
};
|
||||
|
||||
if (includeVCard) {
|
||||
parsedContact['d:propstat']['d:prop']['card:address-data'] = escapeXml(formatContactToVCard([contact]));
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [parsedContact],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:card': 'urn:ietf:params:xml:ns:carddav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml } from '/lib/utils/misc.ts';
|
||||
import { getColorAsHex } from '/lib/utils/calendar.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
import { getCalendars } from '/lib/data/calendar.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype'?: {
|
||||
'd:collection': {};
|
||||
'cal:calendar'?: {};
|
||||
};
|
||||
'd:displayname'?: string | {};
|
||||
'd:getetag'?: string | {};
|
||||
'cs:getctag'?: string | {};
|
||||
'd:current-user-principal'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'd:principal-URL'?: {};
|
||||
'd:current-user-privilege-set'?: {
|
||||
'd:privilege': {
|
||||
'd:write-properties'?: {};
|
||||
'd:write'?: {};
|
||||
'd:write-content'?: {};
|
||||
'd:unlock'?: {};
|
||||
'd:bind'?: {};
|
||||
'd:unbind'?: {};
|
||||
'd:write-acl'?: {};
|
||||
'd:read'?: {};
|
||||
'd:read-acl'?: {};
|
||||
'd:read-current-user-privilege-set'?: {};
|
||||
}[];
|
||||
};
|
||||
'ic:calendar-color'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:cal': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
'xmlns:cs': string;
|
||||
'xmlns:ic': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND' && request.method !== 'REPORT') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [
|
||||
{
|
||||
'd:href': '/dav/calendars/',
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': { 'd:principal-URL': {}, 'd:displayname': {}, 'cs:getctag': {} },
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:cal': 'urn:ietf:params:xml:ns:caldav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
'xmlns:cs': 'http://calendarserver.org/ns/',
|
||||
'xmlns:ic': 'http://apple.com/ns/ical/',
|
||||
},
|
||||
};
|
||||
|
||||
const calendars = await getCalendars(context.state.user.id);
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
const includePrivileges = requestBody.includes('current-user-privilege-set');
|
||||
const includeColor = requestBody.includes('calendar-color');
|
||||
|
||||
if (includePrivileges) {
|
||||
responseBody['d:multistatus']['d:response'][0]['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = {
|
||||
'd:privilege': [
|
||||
{ 'd:write-properties': {} },
|
||||
{ 'd:write': {} },
|
||||
{ 'd:write-content': {} },
|
||||
{ 'd:unlock': {} },
|
||||
{ 'd:bind': {} },
|
||||
{ 'd:unbind': {} },
|
||||
{ 'd:write-acl': {} },
|
||||
{ 'd:read': {} },
|
||||
{ 'd:read-acl': {} },
|
||||
{ 'd:read-current-user-privilege-set': {} },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
for (const calendar of calendars) {
|
||||
const parsedCalendar: DavResponse = {
|
||||
'd:href': `/dav/calendars/${calendar.id}`,
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
'cal:calendar': {},
|
||||
},
|
||||
'd:displayname': calendar.name,
|
||||
'd:getetag': escapeHtml(`"${calendar.revision}"`),
|
||||
'cs:getctag': calendar.revision,
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': { 'd:principal-URL': {} },
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
};
|
||||
|
||||
if (includePrivileges) {
|
||||
parsedCalendar['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = {
|
||||
'd:privilege': [
|
||||
{ 'd:write-properties': {} },
|
||||
{ 'd:write': {} },
|
||||
{ 'd:write-content': {} },
|
||||
{ 'd:unlock': {} },
|
||||
{ 'd:bind': {} },
|
||||
{ 'd:unbind': {} },
|
||||
{ 'd:write-acl': {} },
|
||||
{ 'd:read': {} },
|
||||
{ 'd:read-acl': {} },
|
||||
{ 'd:read-current-user-privilege-set': {} },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (includeColor) {
|
||||
parsedCalendar['d:propstat'][0]['d:prop']['ic:calendar-color'] = getColorAsHex(calendar.color);
|
||||
}
|
||||
|
||||
responseBody['d:multistatus']['d:response'].push(parsedCalendar);
|
||||
}
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,234 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
import { parse } from 'xml/mod.ts';
|
||||
|
||||
import { Calendar, FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts';
|
||||
import { formatCalendarEventsToVCalendar, parseVCalendarFromTextContents } from '/lib/utils/calendar.ts';
|
||||
import {
|
||||
createCalendar,
|
||||
createCalendarEvent,
|
||||
getCalendar,
|
||||
getCalendarEvent,
|
||||
getCalendarEvents,
|
||||
updateCalendarEvent,
|
||||
} from '/lib/data/calendar.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype'?: {
|
||||
'd:collection'?: {};
|
||||
'cal:calendar'?: {};
|
||||
};
|
||||
'd:displayname'?: string | {};
|
||||
'cal:calendar-data'?: string;
|
||||
'd:getlastmodified'?: string;
|
||||
'd:getetag'?: string;
|
||||
'cs:getctag'?: string;
|
||||
'd:getcontenttype'?: string;
|
||||
'd:getcontentlength'?: {};
|
||||
'd:creationdate'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:cal': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
'xmlns:cs': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
request.method !== 'PROPFIND' && request.method !== 'REPORT' && request.method !== 'GET' &&
|
||||
request.method !== 'PUT' && request.method !== 'MKCALENDAR'
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const { calendarId } = context.params;
|
||||
|
||||
let calendar: Calendar | null = null;
|
||||
|
||||
try {
|
||||
calendar = await getCalendar(calendarId, context.state.user.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!calendar && request.method === 'MKCALENDAR') {
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
try {
|
||||
const parsedDocument = parse(requestBody);
|
||||
|
||||
const makeCalendarRequest = (parsedDocument['c:mkcalendar'] || parsedDocument['cal:mkcalendar']) as
|
||||
| Record<string, any>
|
||||
| undefined;
|
||||
|
||||
const name: string = makeCalendarRequest!['d:set']['d:prop']['d:displayname']!;
|
||||
|
||||
const calendar = await createCalendar(context.state.user.id, name);
|
||||
|
||||
return new Response('Created', { status: 201, headers: { 'etag': `"${calendar.revision}"` } });
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse XML`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (request.method === 'PUT') {
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody);
|
||||
|
||||
if (partialCalendarEvent.title && partialCalendarEvent.start_date && partialCalendarEvent.end_date) {
|
||||
const newCalendarEvent = await createCalendarEvent(
|
||||
context.state.user.id,
|
||||
calendarId,
|
||||
partialCalendarEvent.title,
|
||||
new Date(partialCalendarEvent.start_date),
|
||||
new Date(partialCalendarEvent.end_date),
|
||||
partialCalendarEvent.is_all_day,
|
||||
);
|
||||
|
||||
const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {});
|
||||
|
||||
if (parsedExtra !== '{}') {
|
||||
newCalendarEvent.extra = partialCalendarEvent.extra!;
|
||||
|
||||
if (
|
||||
newCalendarEvent.extra.is_recurring && newCalendarEvent.extra.recurring_sequence === 0 &&
|
||||
!newCalendarEvent.extra.recurring_id
|
||||
) {
|
||||
newCalendarEvent.extra.recurring_id = newCalendarEvent.id;
|
||||
}
|
||||
|
||||
await updateCalendarEvent(newCalendarEvent);
|
||||
}
|
||||
|
||||
const calendarEvent = await getCalendarEvent(newCalendarEvent.id, context.state.user.id);
|
||||
|
||||
return new Response('Created', { status: 201, headers: { 'etag': `"${calendarEvent.revision}"` } });
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const calendarEvents = await getCalendarEvents(context.state.user.id, [calendar.id]);
|
||||
|
||||
if (request.method === 'GET') {
|
||||
const response = new Response(formatCalendarEventsToVCalendar(calendarEvents, [calendar]), {
|
||||
status: 200,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
}
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
const includeVCalendar = requestBody.includes('calendar-data');
|
||||
|
||||
const parsedCalendar: DavResponse = {
|
||||
'd:href': `/dav/calendars/${calendar.id}`,
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:displayname': calendar.name,
|
||||
'd:getlastmodified': buildRFC822Date(calendar.updated_at.toISOString()),
|
||||
'd:getetag': escapeHtml(`"${calendar.revision}"`),
|
||||
'cs:getctag': calendar.revision,
|
||||
'd:getcontenttype': 'text/calendar; charset=utf-8',
|
||||
'd:resourcetype': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}],
|
||||
};
|
||||
|
||||
const parsedCalendarEvents = calendarEvents.map((calendarEvent) => {
|
||||
const parsedCalendarEvent: DavResponse = {
|
||||
'd:href': `/dav/calendars/${calendar!.id}/${calendarEvent.id}.ics`,
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:getlastmodified': buildRFC822Date(calendarEvent.updated_at.toISOString()),
|
||||
'd:getetag': escapeHtml(`"${calendarEvent.revision}"`),
|
||||
'cs:getctag': calendarEvent.revision,
|
||||
'd:getcontenttype': 'text/calendar; charset=utf-8',
|
||||
'd:resourcetype': {},
|
||||
'd:creationdate': buildRFC822Date(calendarEvent.created_at.toISOString()),
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': {
|
||||
'd:displayname': {},
|
||||
'd:getcontentlength': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
};
|
||||
|
||||
if (includeVCalendar) {
|
||||
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
||||
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
|
||||
);
|
||||
}
|
||||
|
||||
return parsedCalendarEvent;
|
||||
});
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [parsedCalendar, ...parsedCalendarEvents],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:cal': 'urn:ietf:params:xml:ns:caldav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
'xmlns:cs': 'http://calendarserver.org/ns/',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,253 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { Calendar, CalendarEvent, FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts';
|
||||
import { formatCalendarEventsToVCalendar, parseVCalendarFromTextContents } from '/lib/utils/calendar.ts';
|
||||
import {
|
||||
createCalendarEvent,
|
||||
deleteCalendarEvent,
|
||||
getCalendar,
|
||||
getCalendarEvent,
|
||||
updateCalendarEvent,
|
||||
} from '/lib/data/calendar.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype'?: {
|
||||
'd:collection'?: {};
|
||||
'cal:calendar'?: {};
|
||||
};
|
||||
'd:displayname'?: string | {};
|
||||
'cal:calendar-data'?: string;
|
||||
'd:getlastmodified'?: string;
|
||||
'd:getetag'?: string;
|
||||
'cs:getctag'?: string;
|
||||
'd:getcontenttype'?: string;
|
||||
'd:getcontentlength'?: {};
|
||||
'd:creationdate'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:cal': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
'xmlns:cs': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
request.method !== 'PROPFIND' && request.method !== 'REPORT' && request.method !== 'GET' &&
|
||||
request.method !== 'PUT' && request.method !== 'DELETE'
|
||||
) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const { calendarId, calendarEventId } = context.params;
|
||||
|
||||
let calendar: Calendar | null = null;
|
||||
let calendarEvent: CalendarEvent | null = null;
|
||||
|
||||
try {
|
||||
calendar = await getCalendar(calendarId, context.state.user.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!calendar) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!calendarEvent) {
|
||||
if (request.method === 'PUT') {
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody);
|
||||
|
||||
if (partialCalendarEvent.title && partialCalendarEvent.start_date && partialCalendarEvent.end_date) {
|
||||
const newCalendarEvent = await createCalendarEvent(
|
||||
context.state.user.id,
|
||||
calendarId,
|
||||
partialCalendarEvent.title,
|
||||
new Date(partialCalendarEvent.start_date),
|
||||
new Date(partialCalendarEvent.end_date),
|
||||
partialCalendarEvent.is_all_day,
|
||||
);
|
||||
|
||||
// Use the sent id for the UID
|
||||
if (!partialCalendarEvent.extra?.uid) {
|
||||
partialCalendarEvent.extra = {
|
||||
...(partialCalendarEvent.extra! || {}),
|
||||
uid: calendarEventId,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {});
|
||||
|
||||
if (parsedExtra !== '{}') {
|
||||
newCalendarEvent.extra = partialCalendarEvent.extra!;
|
||||
|
||||
if (
|
||||
newCalendarEvent.extra.is_recurring && newCalendarEvent.extra.recurring_sequence === 0 &&
|
||||
!newCalendarEvent.extra.recurring_id
|
||||
) {
|
||||
newCalendarEvent.extra.recurring_id = newCalendarEvent.id;
|
||||
}
|
||||
|
||||
await updateCalendarEvent(newCalendarEvent);
|
||||
}
|
||||
|
||||
const calendarEvent = await getCalendarEvent(newCalendarEvent.id, context.state.user.id);
|
||||
|
||||
return new Response('Created', { status: 201, headers: { 'etag': `"${calendarEvent.revision}"` } });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (request.method === 'DELETE') {
|
||||
const clientRevision = request.headers.get('if-match') || request.headers.get('etag');
|
||||
|
||||
// Don't update outdated data
|
||||
if (clientRevision && clientRevision !== `"${calendarEvent.revision}"`) {
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } });
|
||||
}
|
||||
|
||||
await deleteCalendarEvent(calendarEventId, calendarId, context.state.user.id);
|
||||
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
if (request.method === 'PUT') {
|
||||
const clientRevision = request.headers.get('if-match') || request.headers.get('etag');
|
||||
|
||||
// Don't update outdated data
|
||||
if (clientRevision && clientRevision !== `"${calendarEvent.revision}"`) {
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } });
|
||||
}
|
||||
|
||||
const requestBody = await request.clone().text();
|
||||
|
||||
const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody);
|
||||
|
||||
calendarEvent = {
|
||||
...calendarEvent,
|
||||
...partialCalendarEvent,
|
||||
};
|
||||
|
||||
await updateCalendarEvent(calendarEvent);
|
||||
|
||||
calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
|
||||
|
||||
return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } });
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
// Set a UID if there isn't one
|
||||
if (!calendarEvent.extra.uid) {
|
||||
calendarEvent.extra.uid = crypto.randomUUID();
|
||||
await updateCalendarEvent(calendarEvent);
|
||||
|
||||
calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
|
||||
}
|
||||
|
||||
const response = new Response(formatCalendarEventsToVCalendar([calendarEvent], [calendar]), {
|
||||
status: 200,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
}
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
const includeVCalendar = requestBody.includes('calendar-data');
|
||||
|
||||
const parsedCalendarEvent: DavResponse = {
|
||||
'd:href': `/dav/calendars/${calendar!.id}/${calendarEvent.id}.ics`,
|
||||
'd:propstat': [{
|
||||
'd:prop': {
|
||||
'd:getlastmodified': buildRFC822Date(calendarEvent.updated_at.toISOString()),
|
||||
'd:getetag': escapeHtml(`"${calendarEvent.revision}"`),
|
||||
'cs:getctag': calendarEvent.revision,
|
||||
'd:getcontenttype': 'text/calendar; charset=utf-8',
|
||||
'd:resourcetype': {},
|
||||
'd:creationdate': buildRFC822Date(calendarEvent.created_at.toISOString()),
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
}, {
|
||||
'd:prop': {
|
||||
'd:displayname': {},
|
||||
'd:getcontentlength': {},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 404 Not Found',
|
||||
}],
|
||||
};
|
||||
|
||||
if (includeVCalendar) {
|
||||
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
||||
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
|
||||
);
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [parsedCalendarEvent],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:cal': 'urn:ietf:params:xml:ns:caldav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
'xmlns:cs': 'http://calendarserver.org/ns/',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml } from '/lib/utils/misc.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection'?: {};
|
||||
};
|
||||
'd:getlastmodified': string;
|
||||
'd:getetag': string;
|
||||
'd:getcontentlength'?: number;
|
||||
'd:getcontenttype'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': { 'xmlns:d': string; 'xmlns:s': string; 'xmlns:oc': string; 'xmlns:nc': string };
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: List directories and files in root
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [
|
||||
{
|
||||
'd:href': '/dav/files/',
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
'd:getlastmodified': buildRFC822Date('2020-01-01'),
|
||||
'd:getetag': escapeHtml(`"fake"`),
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml } from '/lib/utils/misc.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {};
|
||||
};
|
||||
'd:current-user-principal'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'd:getlastmodified'?: string;
|
||||
'd:getetag'?: string;
|
||||
};
|
||||
'd:status': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': { 'xmlns:d': string; 'xmlns:s': string; 'xmlns:oc': string; 'xmlns:nc': string };
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND' && request.method !== 'REPORT') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [
|
||||
{
|
||||
'd:href': '/dav/',
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
},
|
||||
{
|
||||
'd:href': '/dav/addressbooks/',
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
},
|
||||
{
|
||||
'd:href': '/dav/files/',
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
},
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
'd:getlastmodified': buildRFC822Date('2020-01-01'),
|
||||
'd:getetag': escapeHtml(`"fake"`),
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
},
|
||||
};
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { convertObjectToDavXml, DAV_RESPONSE_HEADER } from '/lib/utils/misc.ts';
|
||||
import { createSessionCookie } from '/lib/auth.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
interface DavResponse {
|
||||
'd:href': string;
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection'?: {};
|
||||
'd:principal': {};
|
||||
};
|
||||
'd:displayname'?: string;
|
||||
'card:addressbook-home-set'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'cal:calendar-home-set'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'd:current-user-principal'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
'd:principal-URL'?: {
|
||||
'd:href': string;
|
||||
};
|
||||
};
|
||||
'd:status': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DavMultiStatusResponse {
|
||||
'd:multistatus': {
|
||||
'd:response': DavResponse[];
|
||||
};
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': string;
|
||||
'xmlns:s': string;
|
||||
'xmlns:cal': string;
|
||||
'xmlns:cs': string;
|
||||
'xmlns:card': string;
|
||||
'xmlns:oc': string;
|
||||
'xmlns:nc': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResponseBody extends DavMultiStatusResponse {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === 'GET') {
|
||||
return new Response('This is the WebDAV interface. It can only be accessed by WebDAV clients.');
|
||||
}
|
||||
|
||||
if (request.method !== 'PROPFIND' && request.method !== 'REPORT') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const responseBody: ResponseBody = {
|
||||
'd:multistatus': {
|
||||
'd:response': [],
|
||||
},
|
||||
'd:multistatus_attributes': {
|
||||
'xmlns:d': 'DAV:',
|
||||
'xmlns:s': 'http://sabredav.org/ns',
|
||||
'xmlns:cal': 'urn:ietf:params:xml:ns:caldav',
|
||||
'xmlns:cs': 'http://calendarserver.org/ns/',
|
||||
'xmlns:card': 'urn:ietf:params:xml:ns:carddav',
|
||||
'xmlns:oc': 'http://owncloud.org/ns',
|
||||
'xmlns:nc': 'http://nextcloud.org/ns',
|
||||
},
|
||||
};
|
||||
|
||||
if (request.method === 'PROPFIND') {
|
||||
const propResponse: DavResponse = {
|
||||
'd:href': '/dav/principals/',
|
||||
'd:propstat': {
|
||||
'd:prop': {
|
||||
'd:resourcetype': {
|
||||
'd:collection': {},
|
||||
'd:principal': {},
|
||||
},
|
||||
'd:current-user-principal': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
'd:principal-URL': {
|
||||
'd:href': '/dav/principals/',
|
||||
},
|
||||
},
|
||||
'd:status': 'HTTP/1.1 200 OK',
|
||||
},
|
||||
};
|
||||
|
||||
const requestBody = (await request.clone().text()).toLowerCase();
|
||||
|
||||
if (requestBody.includes('displayname')) {
|
||||
propResponse['d:propstat']['d:prop']['d:displayname'] = `${context.state.user.email}`;
|
||||
}
|
||||
|
||||
if (requestBody.includes('addressbook-home-set')) {
|
||||
propResponse['d:propstat']['d:prop']['card:addressbook-home-set'] = {
|
||||
'd:href': `/dav/addressbooks/`,
|
||||
};
|
||||
}
|
||||
|
||||
if (requestBody.includes('calendar-home-set')) {
|
||||
propResponse['d:propstat']['d:prop']['cal:calendar-home-set'] = {
|
||||
'd:href': `/dav/calendars/`,
|
||||
};
|
||||
}
|
||||
|
||||
responseBody['d:multistatus']['d:response'].push(propResponse);
|
||||
}
|
||||
|
||||
const response = new Response(convertObjectToDavXml(responseBody, true), {
|
||||
headers: {
|
||||
'content-type': 'application/xml; charset=utf-8',
|
||||
'dav': DAV_RESPONSE_HEADER,
|
||||
},
|
||||
status: 207,
|
||||
});
|
||||
|
||||
if (context.state.session) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return createSessionCookie(request, context.state.user, response, true);
|
||||
};
|
||||
51
routes/files.tsx
Normal file
51
routes/files.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||
|
||||
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||
import { getDirectories, getFiles } from '/lib/data/files.ts';
|
||||
import FilesWrapper from '/islands/files/FilesWrapper.tsx';
|
||||
|
||||
interface Data {
|
||||
userDirectories: Directory[];
|
||||
userFiles: DirectoryFile[];
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
|
||||
let currentPath = searchParams.get('path') || '/';
|
||||
|
||||
// Send invalid paths back to root
|
||||
if (!currentPath.startsWith('/') || currentPath.includes('../')) {
|
||||
currentPath = '/';
|
||||
}
|
||||
|
||||
// Always append a trailing slash
|
||||
if (!currentPath.endsWith('/')) {
|
||||
currentPath = `${currentPath}/`;
|
||||
}
|
||||
|
||||
const userDirectories = await getDirectories(context.state.user.id, currentPath);
|
||||
|
||||
const userFiles = await getFiles(context.state.user.id, currentPath);
|
||||
|
||||
return await context.render({ userDirectories, userFiles, currentPath });
|
||||
},
|
||||
};
|
||||
|
||||
export default function FilesPage({ data }: PageProps<Data, FreshContextState>) {
|
||||
return (
|
||||
<main>
|
||||
<FilesWrapper
|
||||
initialDirectories={data.userDirectories}
|
||||
initialFiles={data.userFiles}
|
||||
initialPath={data.currentPath}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
47
routes/files/open/[fileName].tsx
Normal file
47
routes/files/open/[fileName].tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Handlers } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { getFile } from '/lib/data/files.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
async GET(request, context) {
|
||||
if (!context.state.user) {
|
||||
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||
}
|
||||
|
||||
const { fileName } = context.params;
|
||||
|
||||
if (!fileName) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
|
||||
let currentPath = searchParams.get('path') || '/';
|
||||
|
||||
// Send invalid paths back to root
|
||||
if (!currentPath.startsWith('/') || currentPath.includes('../')) {
|
||||
currentPath = '/';
|
||||
}
|
||||
|
||||
// Always append a trailing slash
|
||||
if (!currentPath.endsWith('/')) {
|
||||
currentPath = `${currentPath}/`;
|
||||
}
|
||||
|
||||
// TODO: Verify user has read or write access to path/file and get the appropriate ownerUserId
|
||||
|
||||
const fileResult = await getFile(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
||||
|
||||
if (!fileResult.success) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(fileResult.contents!, {
|
||||
status: 200,
|
||||
headers: { 'cache-control': 'no-cache, no-store, must-revalidate', 'content-type': fileResult.contentType! },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
// Nextcloud/ownCloud mimicry
|
||||
export function handler(): Response {
|
||||
return new Response('Redirecting...', {
|
||||
status: 307,
|
||||
headers: { Location: '/dav' },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user