/opt/canhelp/apps/api/src/routes
Edit: /opt/canhelp/apps/api/src/routes/google.ts (4872B)
/**
* Google routes:
* - Google Sign-In is handled natively by Better Auth (socialProviders.google)
* via /api/auth/sign-in/social — no custom routes needed.
*
* - Google Calendar: custom OAuth flow to connect/disconnect Calendar
* and push/import availability, stored in google_calendar_tokens table.
*/
import { Hono } from 'hono'
import { eq } from 'drizzle-orm'
import { requireAuth, type AuthVariables } from '../middleware/auth.js'
import { db } from '../db.js'
import { users, plans } from '@canhelp/db'
import {
getCalendarAuthUrl,
exchangeCode,
storeCalendarToken,
getCalendarStatus,
disconnectCalendar,
importBusyTimes,
runFirstImport,
} from '../lib/google-oauth.js'
const app = new Hono<{ Variables: AuthVariables }>()
const WEB_URL = process.env.WEB_URL || 'http://localhost:3000'
// ─── Calendar status ──────────────────────────────────────────────────────
// GET /google/calendar/status → { connected, email?, googleUserId? }
app.get('/calendar/status', requireAuth, async (c) => {
const user = c.get('user')
const status = await getCalendarStatus(user.id)
return c.json(status)
})
// ─── Calendar connect (OAuth start) ──────────────────────────────────────
// GET /google/calendar/connect → redirects to Google consent screen
app.get('/calendar/connect', requireAuth, async (c) => {
const user = c.get('user')
// Check plan allows Google Calendar
const [dbUser] = await db.select({ planId: users.planId }).from(users).where(eq(users.id, user.id))
if (dbUser?.planId) {
const [plan] = await db.select({ hasGoogleCalendar: plans.hasGoogleCalendar }).from(plans).where(eq(plans.id, dbUser.planId))
if (!plan?.hasGoogleCalendar) {
return c.json({ error: 'Google Calendar requires a Pro or Ultimate plan', code: 'PLAN_UPGRADE_REQUIRED' }, 403)
}
} else {
return c.json({ error: 'Google Calendar requires a Pro or Ultimate plan', code: 'PLAN_UPGRADE_REQUIRED' }, 403)
}
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
return c.json({ error: 'Google OAuth is not configured on this server' }, 503)
}
const url = getCalendarAuthUrl(user.id)
return c.redirect(url)
})
// ─── Calendar OAuth callback ──────────────────────────────────────────────
// GET /google/calendar/callback?code=...&state=userId
app.get('/calendar/callback', async (c) => {
const code = c.req.query('code')
const userId = c.req.query('state')
const error = c.req.query('error')
if (error || !code || !userId) {
return c.redirect(`${WEB_URL}/schedule?gcal=error`)
}
try {
const { tokens, googleUserId, email } = await exchangeCode(code)
if (!tokens.access_token) {
return c.redirect(`${WEB_URL}/schedule?gcal=error`)
}
await storeCalendarToken(userId, tokens, googleUserId, email)
// Auto-import busy times for today + 3 months (fire-and-forget)
runFirstImport(userId).catch((err) => console.error('[gcal] first import error:', err))
return c.redirect(`${WEB_URL}/schedule?gcal=connected`)
} catch (err) {
console.error('[gcal] callback error:', err)
return c.redirect(`${WEB_URL}/schedule?gcal=error`)
}
})
// ─── Calendar disconnect ──────────────────────────────────────────────────
// DELETE /google/calendar/disconnect
app.delete('/calendar/disconnect', requireAuth, async (c) => {
const user = c.get('user')
await disconnectCalendar(user.id)
return c.json({ ok: true })
})
// ─── Manual import ────────────────────────────────────────────────────────
// POST /google/calendar/import
// Import Google Calendar busy times for today + 3 months into specialist_availability
app.post('/calendar/import', requireAuth, async (c) => {
const user = c.get('user')
const status = await getCalendarStatus(user.id)
if (!status.connected) {
return c.json({ error: 'Google Calendar not connected' }, 400)
}
try {
const today = new Date().toISOString().slice(0, 10)
const threeMonths = new Date()
threeMonths.setMonth(threeMonths.getMonth() + 3)
const to = threeMonths.toISOString().slice(0, 10)
const count = await importBusyTimes(user.id, today, to)
return c.json({ ok: true, imported: count })
} catch (err) {
console.error('[gcal] import error:', err)
return c.json({ error: 'Import failed' }, 500)
}
})
export default app