VirtualPR/backend/src/routes/google.js

152 lines
4.8 KiB
JavaScript

import Joi from 'joi';
import Boom from '@hapi/boom';
import { credentials, authResponse } from '../plugins/auth.js';
import { env } from '../config/env.js';
import { User } from '../models/User.js';
import {
applyGoogleTokens,
createCalendarEvent,
disconnectGoogle,
exchangeCode,
getConnectUrl,
googleConfigured,
signConnectState,
verifyConnectState,
} from '../services/google.js';
function pad(n) {
return String(n).padStart(2, '0');
}
function combineDateTime(date, time) {
const normalized = time.length === 5 ? `${time}:00` : time;
return `${date}T${normalized}`;
}
function addOneHour(date, time) {
const [year, month, day] = date.split('-').map(Number);
const [hours, minutes] = time.split(':').map(Number);
const dt = new Date(year, month - 1, day, hours, minutes);
dt.setHours(dt.getHours() + 1);
return {
date: `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`,
time: `${pad(dt.getHours())}:${pad(dt.getMinutes())}`,
};
}
function eventRange({ date, startTime, endTime }) {
const start = combineDateTime(date, startTime);
let endDate = date;
let resolvedEnd = endTime;
if (!resolvedEnd) {
const next = addOneHour(date, startTime);
endDate = next.date;
resolvedEnd = next.time;
}
let end = combineDateTime(endDate, resolvedEnd);
if (end <= start) {
const [year, month, day] = date.split('-').map(Number);
const nextDay = new Date(year, month - 1, day + 1);
end = combineDateTime(`${nextDay.getFullYear()}-${pad(nextDay.getMonth() + 1)}-${pad(nextDay.getDate())}`, resolvedEnd);
}
return { start, end };
}
function profileRedirect(status) {
return `${env.APP_URL}/profile?google=${encodeURIComponent(status)}`;
}
export function googleRoutes(server) {
server.route({
method: 'GET',
path: '/api/google/connect',
handler: async (request) => {
if (!googleConfigured()) throw Boom.badRequest('Google Calendar is not configured');
const { user } = credentials(request);
return { url: getConnectUrl(signConnectState(user._id)) };
},
});
server.route({
method: 'GET',
path: '/api/google/callback',
options: {
auth: false,
validate: {
query: Joi.object({
code: Joi.string().allow(''),
state: Joi.string().allow(''),
error: Joi.string().allow(''),
scope: Joi.string().allow(''),
}).unknown(true),
},
},
handler: async (request, h) => {
const { code, state, error } = request.query;
if (error || !code || !state) return h.redirect(profileRedirect('error'));
const userId = verifyConnectState(state);
if (!userId) return h.redirect(profileRedirect('error'));
try {
const user = await User.findById(userId);
if (!user || !user.isActive) return h.redirect(profileRedirect('error'));
const { tokens, profile } = await exchangeCode(code);
applyGoogleTokens(user, tokens, profile);
await user.save();
return h.redirect(profileRedirect('connected'));
} catch {
return h.redirect(profileRedirect('error'));
}
},
});
server.route({
method: 'POST',
path: '/api/google/events',
options: {
validate: {
payload: Joi.object({
title: Joi.string().trim().min(1).max(200).required(),
date: Joi.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required(),
startTime: Joi.string().pattern(/^\d{2}:\d{2}$/).required(),
endTime: Joi.string().pattern(/^\d{2}:\d{2}$/).allow('', null),
timeZone: Joi.string().trim().min(1).max(100).allow('', null),
autoAdmit: Joi.boolean().default(true),
allowMicVideo: Joi.boolean().default(true),
}),
},
},
handler: async (request) => {
const { user } = credentials(request);
const fresh = await User.findById(user._id);
if (!fresh || !fresh.isActive) throw Boom.unauthorized();
if (!fresh.google?.refreshToken && !fresh.google?.accessToken) {
throw Boom.badRequest('Connect Google Calendar first');
}
const { title, date, startTime, endTime, timeZone, autoAdmit, allowMicVideo } = request.payload;
const { start, end } = eventRange({ date, startTime, endTime });
return createCalendarEvent(fresh, {
title,
start,
end,
timeZone: timeZone || 'UTC',
autoAdmit,
allowMicVideo,
});
},
});
server.route({
method: 'POST',
path: '/api/google/disconnect',
handler: async (request) => {
const { user } = credentials(request);
const fresh = await User.findById(user._id).populate('role');
if (!fresh || !fresh.isActive) throw Boom.unauthorized();
await disconnectGoogle(fresh);
await fresh.populate('role');
const { token, ...rest } = authResponse(fresh, 'unused');
return rest;
},
});
}