Files
accounted/app/api/calendar/feed/route.ts
T
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00

168 lines
4.4 KiB
TypeScript

import { NextResponse } from 'next/server'
import { z } from 'zod'
import { withRouteContext } from '@/lib/api/with-route-context'
import { validateBody } from '@/lib/api/validate'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
// Only the two content toggles are user-settable. Strict: the previous
// implementation passed the raw JSON body into .update(), which would have
// let a caller set feed_token (token fixation on a public URL), expires_at,
// or access_count.
const UpdateFeedSchema = z
.object({
include_tax_deadlines: z.boolean().optional(),
include_invoices: z.boolean().optional(),
})
.strict()
.refine(
(v) => v.include_tax_deadlines !== undefined || v.include_invoices !== undefined,
{ message: 'Nothing to update' },
)
function feedUrls(feedToken: string) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://app.erp-base.se'
return {
webcalUrl: `webcal://${baseUrl.replace(/^https?:\/\//, '')}/api/calendar/feed/${feedToken}`,
httpsUrl: `${baseUrl}/api/calendar/feed/${feedToken}`,
}
}
/**
* GET /api/calendar/feed
* Get current user's calendar feed settings
*/
export const GET = withRouteContext('calendar_feed.get', async (_request, ctx) => {
const { supabase, companyId } = ctx
const { data: feed, error } = await supabase
.from('calendar_feeds')
.select('*')
.eq('company_id', companyId)
.single()
if (error && error.code !== 'PGRST116') {
// PGRST116 = no rows returned, which is fine
return NextResponse.json({ error: getUserErrorMessage(error) }, { status: 500 })
}
if (feed) {
return NextResponse.json({
data: { ...feed, ...feedUrls(feed.feed_token) },
})
}
return NextResponse.json({ data: null })
})
/**
* POST /api/calendar/feed
* Create a new calendar feed for the current user
*/
export const POST = withRouteContext(
'calendar_feed.create',
async (_request, ctx) => {
const { supabase, companyId, user } = ctx
// Check if feed already exists
const { data: existingFeed } = await supabase
.from('calendar_feeds')
.select('id')
.eq('company_id', companyId)
.single()
if (existingFeed) {
return NextResponse.json(
{ error: 'Calendar feed already exists' },
{ status: 409 }
)
}
// Create new feed
const { data: feed, error } = await supabase
.from('calendar_feeds')
.insert({
user_id: user.id,
company_id: companyId,
is_active: true,
include_tax_deadlines: true,
include_invoices: true,
})
.select()
.single()
if (error) {
return NextResponse.json({ error: getUserErrorMessage(error) }, { status: 500 })
}
return NextResponse.json({
data: { ...feed, ...feedUrls(feed.feed_token) },
})
},
{ requireWrite: true },
)
/**
* PUT /api/calendar/feed
* Update calendar feed settings
*/
export const PUT = withRouteContext(
'calendar_feed.update',
async (request, ctx) => {
const { supabase, companyId, log } = ctx
const validation = await validateBody(request, UpdateFeedSchema, {
log,
operation: 'calendar_feed.update',
})
if (!validation.success) return validation.response
const { data: feed, error } = await supabase
.from('calendar_feeds')
.update(validation.data)
.eq('company_id', companyId)
.select()
.single()
if (error) {
return NextResponse.json({ error: getUserErrorMessage(error) }, { status: 500 })
}
return NextResponse.json({
data: { ...feed, ...feedUrls(feed.feed_token) },
})
},
{ requireWrite: true },
)
/**
* DELETE /api/calendar/feed
* Regenerate calendar feed token (invalidates old URL)
*/
export const DELETE = withRouteContext(
'calendar_feed.rotate_token',
async (_request, ctx) => {
const { supabase, companyId } = ctx
// Generate a new token by updating with a new UUID
const { data: feed, error } = await supabase
.from('calendar_feeds')
.update({
feed_token: crypto.randomUUID(),
access_count: 0,
last_accessed_at: null,
})
.eq('company_id', companyId)
.select()
.single()
if (error) {
return NextResponse.json({ error: getUserErrorMessage(error) }, { status: 500 })
}
return NextResponse.json({
data: { ...feed, ...feedUrls(feed.feed_token) },
})
},
{ requireWrite: true },
)