feat: Tier 1 & 2 — Daily Note, Voice, Flashcard quota, Readwise, Calendar, Agent Gallery
Tier 1: - BASIC tier: chat (10/mo) + reformulate (10/mo) désormais accessibles - Nouveaux quotas: ai_flashcard + voice_transcribe dans tous les tiers - /api/notes/daily : note du jour auto-créée (find or create) - Bouton Note du Jour dans la sidebar (CalendarDays) - Voice-to-Text dans l'éditeur (Web Speech API, bouton Mic toolbar) - Flashcard generation → quota ai_flashcard (au lieu de reformulate) Tier 2: - Intégration Readwise: GET/POST/DELETE /api/integrations/readwise - Intégration Google Calendar: OAuth flow + today's events + meeting notes - /api/integrations/calendar + /callback - Page /settings/integrations avec cards Calendar + Readwise - SettingsNav: onglet Intégrations - AgentTemplates: catégories + 4 nouveaux templates (Digest/Recap/AutoTagger/Synthesis) Schema: - UserAISettings.integrationTokens Json? (migration 20260529160000) - prisma generate + migrate deploy appliqués Fix: - SpeechRecognition types (triple-slash @types/dom-speech-recognition) - Notebook.create: suppression champ 'description' inexistant Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
69
memento-note/app/api/integrations/calendar/callback/route.ts
Normal file
69
memento-note/app/api/integrations/calendar/callback/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* GET /api/integrations/calendar/callback
|
||||
* Google OAuth callback — exchanges code for tokens and persists them.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const url = new URL(req.url)
|
||||
const code = url.searchParams.get('code')
|
||||
const userId = url.searchParams.get('state')
|
||||
const error = url.searchParams.get('error')
|
||||
|
||||
if (error || !code || !userId) {
|
||||
return NextResponse.redirect(
|
||||
`${url.origin}/settings/integrations?error=calendar_auth_failed`,
|
||||
)
|
||||
}
|
||||
|
||||
const clientId = process.env.GOOGLE_CALENDAR_CLIENT_ID || process.env.AUTH_GOOGLE_ID || ''
|
||||
const clientSecret = process.env.GOOGLE_CALENDAR_CLIENT_SECRET || process.env.AUTH_GOOGLE_SECRET || ''
|
||||
const redirectUri = `${url.origin}/api/integrations/calendar/callback`
|
||||
|
||||
const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
}),
|
||||
})
|
||||
|
||||
const tokenData = await tokenRes.json()
|
||||
|
||||
if (!tokenData.access_token) {
|
||||
return NextResponse.redirect(
|
||||
`${url.origin}/settings/integrations?error=calendar_token_failed`,
|
||||
)
|
||||
}
|
||||
|
||||
// Persist tokens in UserAISettings.integrationTokens
|
||||
const aiSettings = await prisma.userAISettings.findUnique({
|
||||
where: { userId },
|
||||
select: { integrationTokens: true },
|
||||
})
|
||||
const meta =
|
||||
typeof aiSettings?.integrationTokens === 'string'
|
||||
? JSON.parse(aiSettings.integrationTokens)
|
||||
: (aiSettings?.integrationTokens as Record<string, unknown> | null) ?? {}
|
||||
|
||||
meta.calendarAccessToken = tokenData.access_token
|
||||
if (tokenData.refresh_token) {
|
||||
meta.calendarRefreshToken = tokenData.refresh_token
|
||||
}
|
||||
|
||||
await prisma.userAISettings.upsert({
|
||||
where: { userId },
|
||||
update: { integrationTokens: JSON.stringify(meta) },
|
||||
create: { userId, integrationTokens: JSON.stringify(meta) },
|
||||
})
|
||||
|
||||
return NextResponse.redirect(`${url.origin}/settings/integrations?connected=calendar`)
|
||||
}
|
||||
Reference in New Issue
Block a user