diff --git a/views/project/_tab_botconfig.php b/views/project/_tab_botconfig.php index 4ff7482..ae6a846 100644 --- a/views/project/_tab_botconfig.php +++ b/views/project/_tab_botconfig.php @@ -40,10 +40,9 @@
Checking…
- + diff --git a/web/api/linkedin_auth.php b/web/api/linkedin_auth.php new file mode 100644 index 0000000..1784fb2 --- /dev/null +++ b/web/api/linkedin_auth.php @@ -0,0 +1,27 @@ + 'project_id required']); exit; } + +$stmt = $db->prepare('SELECT * FROM projects WHERE id = ? AND is_active = 1'); +$stmt->execute([$pid]); +if (!$stmt->fetch()) { http_response_code(404); echo json_encode(['error' => 'Project not found']); exit; } + +$client_id = getenv('LINKEDIN_CLIENT_ID'); +if (!$client_id) { echo json_encode(['error' => 'LINKEDIN_CLIENT_ID not set in server environment']); exit; } + +$state = bin2hex(random_bytes(16)); +$_SESSION['linkedin_oauth'] = ['state' => $state, 'project_id' => $pid]; + +$redirect_uri = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/linkedin/callback'; + +$url = 'https://www.linkedin.com/oauth/v2/authorization?' . http_build_query([ + 'response_type' => 'code', + 'client_id' => $client_id, + 'redirect_uri' => $redirect_uri, + 'state' => $state, + 'scope' => 'w_organization_social openid profile', +]); + +echo json_encode(['url' => $url]); diff --git a/web/assets/js/app.js b/web/assets/js/app.js index 2ee4574..e5dcbfd 100644 --- a/web/assets/js/app.js +++ b/web/assets/js/app.js @@ -2364,6 +2364,7 @@ if (addScanPathForm) { botconfig_mastodon_remove: 'mastodon removed', botconfig_linkedin_page_add: 'linkedin page added', botconfig_linkedin_page_save: 'linkedin page updated', botconfig_linkedin_page_remove: 'linkedin page removed', + linkedin_oauth_connect: 'linkedin connected', file_write: 'file edit', file_delete: 'file delete', file_upload: 'upload', post_create: 'post created', post_delete: 'post deleted', post_publish: 'post published', post_duplicate: 'post duplicated', @@ -2399,6 +2400,7 @@ if (addScanPathForm) { botconfig_rss_add: 'bi-rss', botconfig_rss_save: 'bi-rss', botconfig_rss_remove: 'bi-rss', botconfig_mastodon_add: 'bi-mastodon', botconfig_mastodon_save: 'bi-mastodon', botconfig_mastodon_remove: 'bi-mastodon', botconfig_linkedin_page_add: 'bi-linkedin', botconfig_linkedin_page_save: 'bi-linkedin', botconfig_linkedin_page_remove: 'bi-linkedin', + linkedin_oauth_connect: 'bi-linkedin', file_write: 'bi-pencil', file_delete: 'bi-trash', file_upload: 'bi-cloud-upload', post_create: 'bi-file-earmark-plus', post_delete: 'bi-file-earmark-x', post_publish: 'bi-send', post_duplicate: 'bi-files', @@ -3386,6 +3388,13 @@ window.addEventListener('DOMContentLoaded', () => { expiry.textContent = li.token_expiry ? 'Expires: ' + li.token_expiry : ''; } + document.getElementById('liConnectBtn').addEventListener('click', async () => { + const r = await fetch('/api/linkedin_auth?project_id=' + pid); + const d = await r.json(); + if (d.error) { showError(d.error); return; } + window.location.href = d.url; + }); + function renderLinkedinPages(pages) { const list = document.getElementById('linkedinPagesList'); const names = Object.keys(pages); diff --git a/web/index.php b/web/index.php index ace7bd6..05235e0 100644 --- a/web/index.php +++ b/web/index.php @@ -15,6 +15,13 @@ if ($uri === '/api/auth') { exit; } +// LinkedIn OAuth callback — session required but no full auth check +// (LinkedIn redirects back here; user's browser session is active) +if ($uri === '/linkedin/callback') { + include ROOT . '/web/linkedin_callback.php'; + exit; +} + // All other routes require login Auth::requireLogin(); ProjectTypes::load(); diff --git a/web/linkedin_callback.php b/web/linkedin_callback.php new file mode 100644 index 0000000..c7ac664 --- /dev/null +++ b/web/linkedin_callback.php @@ -0,0 +1,81 @@ +Go back'; + exit; +} + +$pid = (int)($_SESSION['linkedin_oauth']['project_id'] ?? 0); +unset($_SESSION['linkedin_oauth']); + +if (isset($_GET['error'])) { + $msg = htmlspecialchars($_GET['error_description'] ?? $_GET['error']); + echo "LinkedIn denied access: $msg. Go back"; + exit; +} + +$code = $_GET['code'] ?? ''; +if (!$code) { echo 'Missing code. Go back'; exit; } + +$client_id = getenv('LINKEDIN_CLIENT_ID'); +$client_secret = getenv('LINKEDIN_CLIENT_SECRET'); +$redirect_uri = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/linkedin/callback'; + +// Exchange code for tokens +$ch = curl_init('https://www.linkedin.com/oauth/v2/accessToken'); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query([ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $redirect_uri, + 'client_id' => $client_id, + 'client_secret' => $client_secret, + ]), + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], +]); +$body = curl_exec($ch); +$status = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +$data = json_decode($body, true); +if ($status !== 200 || empty($data['access_token'])) { + $err = htmlspecialchars($data['error_description'] ?? $data['error'] ?? 'Unknown error'); + echo "LinkedIn token exchange failed: $err. Go back"; + exit; +} + +// Get the project's config.json path +$stmt = $db->prepare('SELECT path FROM projects WHERE id = ? AND is_active = 1'); +$stmt->execute([$pid]); +$project = $stmt->fetch(); +if (!$project) { echo 'Project not found. Go back'; exit; } + +$config_path = realpath($project['path']) . '/data/config.json'; + +if (!file_exists($config_path)) { + echo "config.json not found at $config_path. Go back"; + exit; +} + +$config = json_decode(file_get_contents($config_path), true); +if (!is_array($config)) { echo 'Could not parse config.json. Go back'; exit; } + +if (!isset($config['linkedin'])) $config['linkedin'] = []; +$config['linkedin']['access_token'] = $data['access_token']; +$config['linkedin']['refresh_token'] = $data['refresh_token'] ?? null; +$expires_in = $data['expires_in'] ?? 5184000; +$config['linkedin']['token_expiry'] = date('c', time() + $expires_in); + +$tmp = $config_path . '.tmp'; +file_put_contents($tmp, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +rename($tmp, $config_path); + +Audit::log($db, 'linkedin_oauth_connect', $pid); + +header('Location: /project/' . $pid . '?tab=botconfig'); +exit;