Files CRUD.

Remove Contacts and Calendar + CardDav and CalDav.
This commit is contained in:
Bruno Bernardino
2024-04-03 14:02:04 +01:00
parent c4788761d2
commit 4e5fdd569a
89 changed files with 2302 additions and 8001 deletions

View File

@@ -1,7 +0,0 @@
// Nextcloud/ownCloud mimicry
export function handler(): Response {
return new Response('Redirecting...', {
status: 307,
headers: { Location: '/dav' },
});
}

View File

@@ -1,7 +0,0 @@
// Nextcloud/ownCloud mimicry
export function handler(): Response {
return new Response('Redirecting...', {
status: 307,
headers: { Location: '/dav' },
});
}

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View File

@@ -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));
},
};

View 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));
},
};

View 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));
},
};

View 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));
},
};

View 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));
},
};

View 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
View 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));
},
};

View 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));
},
};

View 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));
},
};

View 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));
},
};

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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
View 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>
);
}

View 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! },
});
},
};

View File

@@ -1,7 +0,0 @@
// Nextcloud/ownCloud mimicry
export function handler(): Response {
return new Response('Redirecting...', {
status: 307,
headers: { Location: '/dav' },
});
}