152 lines
4.8 KiB
JavaScript
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;
|
|
},
|
|
});
|
|
}
|