- New server/google_calendar.mjs wrapping googleapis v3 with auto token refresh - Four new actions: calendar-list-events (auto-accept), calendar-create/update/delete-event (queue) - bin/ccc-gcal-auth.mjs one-time OAuth2 consent flow helper - config.example.json updated with google_calendar block - server/config.mjs, index.mjs wired up following the same pattern as SMTP/mailer - Bump version to 1.2.0 - Add CLAUDE.md and claude-info/ with architecture reference, feature plans, and contributing checklist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
87 lines
2.4 KiB
JavaScript
87 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// One-time OAuth2 authorization flow for Google Calendar.
|
|
// Run this on the host to generate a token file for the server.
|
|
//
|
|
// Usage:
|
|
// ccc-gcal-auth --credentials google-credentials.json --token google-token.json
|
|
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { createServer } from 'node:http';
|
|
import { google } from 'googleapis';
|
|
|
|
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
|
|
const PORT = 3016;
|
|
const REDIRECT_URI = `http://localhost:${PORT}/callback`;
|
|
|
|
function get_arg(argv, flag) {
|
|
const i = argv.indexOf(flag);
|
|
return i !== -1 ? argv[i + 1] : null;
|
|
}
|
|
|
|
const credentials_path = get_arg(process.argv, '--credentials');
|
|
const token_path = get_arg(process.argv, '--token');
|
|
|
|
if (!credentials_path || !token_path) {
|
|
console.error('Usage: ccc-gcal-auth --credentials <path> --token <output-path>');
|
|
process.exit(1);
|
|
}
|
|
|
|
let credentials;
|
|
try {
|
|
credentials = JSON.parse(readFileSync(credentials_path, 'utf8'));
|
|
} catch (err) {
|
|
console.error(`Cannot read credentials file: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const { client_id, client_secret } = credentials.installed ?? credentials.web;
|
|
const oauth2_client = new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
|
|
|
|
const auth_url = oauth2_client.generateAuthUrl({
|
|
access_type: 'offline',
|
|
scope: SCOPES,
|
|
prompt: 'consent',
|
|
});
|
|
|
|
console.log('Open this URL in your browser to authorize Google Calendar access:\n');
|
|
console.log(auth_url);
|
|
console.log(`\nListening for redirect on http://localhost:${PORT}/callback ...`);
|
|
|
|
const server = createServer(async (req, res) => {
|
|
let url;
|
|
try {
|
|
url = new URL(req.url, `http://localhost:${PORT}`);
|
|
} catch (_) {
|
|
res.end('Bad request');
|
|
return;
|
|
}
|
|
|
|
if (url.pathname !== '/callback') {
|
|
res.end('Not found');
|
|
return;
|
|
}
|
|
|
|
const code = url.searchParams.get('code');
|
|
if (!code) {
|
|
res.writeHead(400);
|
|
res.end('Missing code parameter');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { tokens } = await oauth2_client.getToken(code);
|
|
writeFileSync(token_path, JSON.stringify(tokens, null, '\t') + '\n', 'utf8');
|
|
res.end('Authorization successful. You may close this tab.');
|
|
console.log(`\nToken written to ${token_path}`);
|
|
server.close();
|
|
} catch (err) {
|
|
res.writeHead(500);
|
|
res.end(`Error: ${err.message}`);
|
|
console.error(`Token exchange failed: ${err.message}`);
|
|
server.close();
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, '127.0.0.1');
|