My-Weekly-ToDo-List/src/app/api/user/profile/route.ts
mARTin-B78 02b2e40649 feat: add customizable CSS variables and custom CSS to Styling settings
Adds a "CSS Variables" panel to the Styling tab with a color picker and
description for every color token in globals.css (base, sidebar, design
token aliases, event colors, all-day chips), plus a "Custom CSS" textarea
that loads after the default stylesheet and can override any rule.

Overrides are stored per-user (customCssVars, customCss) and applied on
the root container's inline style, so they take precedence over themes
and per-field settings.

v1.113.0
2026-07-08 19:57:21 +02:00

403 lines
18 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
// Get user profile
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const user = await (prisma.user as any).findUnique({
where: { email: session.user.email },
select: {
id: true,
name: true,
email: true,
timezone: true,
autoRolling: true,
protectEventTimes: true,
showCalendarProviderIcon: true,
language: true,
dateFormat: true,
timeFormat: true,
startHour: true,
endHour: true,
showNextTask: true,
calendarEditMode: true,
focusTimerDuration: true,
focusBreakDuration: true,
showTimeGrid: true,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
showTaskCheckboxes: true,
showProjectIcons: true,
showPriorityIcons: true,
priorityStyle: true,
cellDuration: true,
viewStyle: true,
viewDays: true,
fontSize: true,
goalFallbackType: true,
goalDefaultSentence: true,
goalFontFamily: true,
goalFontSize: true,
goalFontWeight: true,
goalScope: true,
dateLayout: true,
mobileDateLayout: true,
dateVerticalAlign: true,
headerDisplay: true,
headerCustomFormat: true,
headerCurrentDayFormat: true,
mobilePortraitHeaderDisplay: true,
mobileLandscapeHeaderDisplay: true,
headlineFont: true,
headlineFontSize: true,
headlineFontWeight: true,
dateFontFamily: true,
dateFontSize: true,
dateFontWeight: true,
timeTaskFontFamily: true,
timeTaskFontSize: true,
timeTaskFontWeight: true,
bodyFont: true,
taskFontFamily: true,
taskFontSize: true,
taskFontWeight: true,
eventFontFamily: true,
eventFontSize: true,
eventFontWeight: true,
fontWeight: true,
weekdayColor: true,
dateColor: true,
taskColor: true,
todayHighlightColor: true,
weekendColorSat: true,
weekendColorSun: true,
pastDayColor: true,
hourLabelFormat: true,
showSubHourSlots: true,
allDayPosition: true,
cwFontFamily: true,
cwFontSize: true,
cwFontWeight: true,
cwColor: true,
yearFontFamily: true,
yearFontSize: true,
yearFontWeight: true,
yearColor: true,
dayHeaderGap: true,
showCompletedTasks: true,
showLines: true,
startDayOffset: true,
weekdayFormat: true,
weekdayCase: true,
customWeekdayNames: true,
weekStartDay: true,
quoteSourceUrls: true,
quoteLanguages: true,
kanbanStages: true,
accountNumber: true,
lightTheme: true,
darkTheme: true,
notificationsEnabled: true,
mobileFontScale: true,
weatherEnabled: true,
weatherLat: true,
weatherLon: true,
weatherLocation: true,
weatherRecentCities: true,
viewSettings: true,
customCssVars: true,
customCss: true,
hasCompletedOnboarding: true,
createdAt: true
}
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json({ user });
}
// Update user profile
export async function PATCH(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session || !session.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
const {
name, timezone, password, autoRolling, protectEventTimes, showCalendarProviderIcon,
language, dateFormat, timeFormat, startHour, endHour,
showNextTask, calendarEditMode, focusTimerDuration, focusBreakDuration,
showTimeGrid, showSomeday, showAllDayEvents, showSchedule,
cellDuration, viewStyle, viewDays, fontSize,
headlineFont, headlineFontSize, headlineFontWeight,
dateFontFamily, dateFontSize, dateFontWeight,
timeTaskFontFamily, timeTaskFontSize, timeTaskFontWeight,
bodyFont, taskFontFamily, taskFontSize, taskFontWeight,
eventFontFamily, eventFontSize, eventFontWeight,
fontWeight, weekendColorSat, weekendColorSun,
weekdayColor, dateColor, taskColor, todayHighlightColor,
pastDayColor, goalFallbackType, goalDefaultSentence,
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat, headerCurrentDayFormat, mobilePortraitHeaderDisplay, mobileLandscapeHeaderDisplay,
hourLabelFormat, showSubHourSlots, allDayPosition,
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
showTaskCheckboxes, showProjectIcons, showPriorityIcons, priorityStyle, dayHeaderGap,
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages,
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
customCssVars, customCss,
hasCompletedOnboarding
} = body;
const updateData: any = {
...(name !== undefined && { name }),
...(timezone !== undefined && { timezone }),
...(autoRolling !== undefined && { autoRolling }),
...(protectEventTimes !== undefined && { protectEventTimes }),
...(showCalendarProviderIcon !== undefined && { showCalendarProviderIcon }),
...(language !== undefined && { language }),
...(dateFormat !== undefined && { dateFormat }),
...(timeFormat !== undefined && { timeFormat }),
...(startHour !== undefined && !isNaN(startHour) && { startHour }),
...(endHour !== undefined && !isNaN(endHour) && { endHour }),
...(showNextTask !== undefined && { showNextTask }),
...(calendarEditMode !== undefined && { calendarEditMode }),
...(focusTimerDuration !== undefined && !isNaN(focusTimerDuration) && { focusTimerDuration }),
...(focusBreakDuration !== undefined && !isNaN(focusBreakDuration) && { focusBreakDuration }),
...(showTimeGrid !== undefined && { showTimeGrid }),
...(showSomeday !== undefined && { showSomeday }),
...(showAllDayEvents !== undefined && { showAllDayEvents }),
...(showSchedule !== undefined && { showSchedule }),
...(showTaskCheckboxes !== undefined && { showTaskCheckboxes }),
...(showProjectIcons !== undefined && { showProjectIcons }),
...(showPriorityIcons !== undefined && { showPriorityIcons }),
...(priorityStyle !== undefined && { priorityStyle }),
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
...(viewStyle !== undefined && { viewStyle }),
...(viewDays !== undefined && !isNaN(viewDays) && { viewDays }),
...(fontSize !== undefined && { fontSize }),
...(headlineFont !== undefined && { headlineFont }),
...(headlineFontSize !== undefined && { headlineFontSize }),
...(headlineFontWeight !== undefined && { headlineFontWeight }),
...(dateFontFamily !== undefined && { dateFontFamily }),
...(dateFontSize !== undefined && { dateFontSize }),
...(dateFontWeight !== undefined && { dateFontWeight }),
...(timeTaskFontFamily !== undefined && { timeTaskFontFamily }),
...(timeTaskFontSize !== undefined && { timeTaskFontSize }),
...(timeTaskFontWeight !== undefined && { timeTaskFontWeight }),
...(bodyFont !== undefined && { bodyFont }),
...(taskFontFamily !== undefined && { taskFontFamily }),
...(taskFontSize !== undefined && { taskFontSize }),
...(taskFontWeight !== undefined && { taskFontWeight }),
...(eventFontFamily !== undefined && { eventFontFamily }),
...(eventFontSize !== undefined && { eventFontSize }),
...(eventFontWeight !== undefined && { eventFontWeight }),
...(fontWeight !== undefined && { fontWeight }),
...(weekdayColor !== undefined && { weekdayColor }),
...(dateColor !== undefined && { dateColor }),
...(taskColor !== undefined && { taskColor }),
...(todayHighlightColor !== undefined && { todayHighlightColor }),
...(weekendColorSat !== undefined && { weekendColorSat }),
...(weekendColorSun !== undefined && { weekendColorSun }),
...(pastDayColor !== undefined && { pastDayColor }),
...(goalFallbackType !== undefined && { goalFallbackType }),
...(goalDefaultSentence !== undefined && { goalDefaultSentence }),
...(goalFontFamily !== undefined && { goalFontFamily }),
...(goalFontSize !== undefined && { goalFontSize }),
...(goalFontWeight !== undefined && { goalFontWeight }),
...(goalScope !== undefined && { goalScope }),
...(dateLayout !== undefined && { dateLayout }),
...(mobileDateLayout !== undefined && { mobileDateLayout }),
...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
...(headerDisplay !== undefined && { headerDisplay }),
...(headerCustomFormat !== undefined && { headerCustomFormat }),
...(headerCurrentDayFormat !== undefined && { headerCurrentDayFormat }),
...(mobilePortraitHeaderDisplay !== undefined && { mobilePortraitHeaderDisplay }),
...(mobileLandscapeHeaderDisplay !== undefined && { mobileLandscapeHeaderDisplay }),
...(hourLabelFormat !== undefined && { hourLabelFormat }),
...(showSubHourSlots !== undefined && { showSubHourSlots }),
...(allDayPosition !== undefined && { allDayPosition }),
...(cwFontFamily !== undefined && { cwFontFamily }),
...(cwFontSize !== undefined && { cwFontSize }),
...(cwFontWeight !== undefined && { cwFontWeight }),
...(cwColor !== undefined && { cwColor }),
...(yearFontFamily !== undefined && { yearFontFamily }),
...(yearFontSize !== undefined && { yearFontSize }),
...(yearFontWeight !== undefined && { yearFontWeight }),
...(yearColor !== undefined && { yearColor }),
...(dayHeaderGap !== undefined && { dayHeaderGap }),
...(showCompletedTasks !== undefined && { showCompletedTasks }),
...(showLines !== undefined && { showLines }),
...(startDayOffset !== undefined && { startDayOffset }),
...(weekdayFormat !== undefined && { weekdayFormat }),
...(weekdayCase !== undefined && { weekdayCase }),
...(customWeekdayNames !== undefined && { customWeekdayNames }),
...(weekStartDay !== undefined && { weekStartDay }),
...(quoteSourceUrls !== undefined && { quoteSourceUrls }),
...(quoteLanguages !== undefined && { quoteLanguages }),
...(kanbanStages !== undefined && { kanbanStages }),
...(lightTheme !== undefined && { lightTheme }),
...(darkTheme !== undefined && { darkTheme }),
...(notificationsEnabled !== undefined && { notificationsEnabled }),
...(mobileFontScale !== undefined && { mobileFontScale: parseFloat(mobileFontScale) || 1.0 }),
...(weatherEnabled !== undefined && { weatherEnabled }),
...(weatherLat !== undefined && { weatherLat: weatherLat !== null ? parseFloat(weatherLat) : null }),
...(weatherLon !== undefined && { weatherLon: weatherLon !== null ? parseFloat(weatherLon) : null }),
...(weatherLocation !== undefined && { weatherLocation }),
...(weatherRecentCities !== undefined && { weatherRecentCities }),
...(viewSettings !== undefined && { viewSettings }),
...(customCssVars !== undefined && { customCssVars }),
...(customCss !== undefined && { customCss }),
...(hasCompletedOnboarding !== undefined && { hasCompletedOnboarding }),
};
if (password && password.trim() !== "") {
updateData.passwordHash = await bcrypt.hash(password, 10);
}
const user = await (prisma.user as any).update({
where: { email: session.user.email },
data: updateData,
select: {
id: true,
name: true,
email: true,
timezone: true,
autoRolling: true,
protectEventTimes: true,
language: true,
dateFormat: true,
timeFormat: true,
startHour: true,
endHour: true,
showNextTask: true,
calendarEditMode: true,
focusTimerDuration: true,
focusBreakDuration: true,
showTimeGrid: true,
showSomeday: true,
showAllDayEvents: true,
showSchedule: true,
showTaskCheckboxes: true,
showProjectIcons: true,
showPriorityIcons: true,
priorityStyle: true,
cellDuration: true,
viewStyle: true,
viewDays: true,
fontSize: true,
headlineFont: true,
headlineFontSize: true,
headlineFontWeight: true,
dateFontFamily: true,
dateFontSize: true,
dateFontWeight: true,
timeTaskFontFamily: true,
timeTaskFontSize: true,
timeTaskFontWeight: true,
bodyFont: true,
taskFontFamily: true,
taskFontSize: true,
taskFontWeight: true,
eventFontFamily: true,
eventFontSize: true,
eventFontWeight: true,
fontWeight: true,
weekdayColor: true,
dateColor: true,
taskColor: true,
todayHighlightColor: true,
weekendColorSat: true,
weekendColorSun: true,
pastDayColor: true,
hourLabelFormat: true,
showSubHourSlots: true,
allDayPosition: true,
goalFallbackType: true,
goalDefaultSentence: true,
goalFontFamily: true,
goalFontSize: true,
goalFontWeight: true,
goalScope: true,
dateLayout: true,
mobileDateLayout: true,
dateVerticalAlign: true,
headerDisplay: true,
headerCustomFormat: true,
headerCurrentDayFormat: true,
mobilePortraitHeaderDisplay: true,
mobileLandscapeHeaderDisplay: true,
cwFontFamily: true,
cwFontSize: true,
cwFontWeight: true,
cwColor: true,
yearFontFamily: true,
yearFontSize: true,
yearFontWeight: true,
yearColor: true,
dayHeaderGap: true,
showCompletedTasks: true,
showLines: true,
startDayOffset: true,
weekdayFormat: true,
weekdayCase: true,
customWeekdayNames: true,
weekStartDay: true,
quoteSourceUrls: true,
quoteLanguages: true,
kanbanStages: true,
accountNumber: true,
lightTheme: true,
darkTheme: true,
notificationsEnabled: true,
mobileFontScale: true,
weatherEnabled: true,
weatherLat: true,
weatherLon: true,
weatherLocation: true,
weatherRecentCities: true,
viewSettings: true,
customCssVars: true,
customCss: true,
hasCompletedOnboarding: true,
}
});
return NextResponse.json({ success: true, user });
} catch (e) {
console.error('Error updating profile:', e);
return NextResponse.json(
{ error: 'Failed to update profile', details: (e as Error).message },
{ status: 500 }
);
}
}
// Delete account
export async function DELETE(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
await prisma.user.delete({
where: { email: session.user.email }
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting account:', error);
return NextResponse.json({ error: 'Failed to delete account' }, { status: 500 });
}
}