Files
scan-receipts/scripts/query_campaign_keywords.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

113 lines
3.5 KiB
JavaScript

import crypto from 'crypto';
import https from 'https';
import fs from 'fs';
const envContent = fs.readFileSync('.env.local', 'utf8');
const env = {};
envContent.split('\n').forEach(line => {
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
let val = match[2].trim();
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
env[match[1].trim()] = val;
}
});
const CLIENT_ID = env.APPLE_SEARCH_ADS_CLIENT_ID;
const TEAM_ID = env.APPLE_SEARCH_ADS_TEAM_ID;
const KEY_ID = env.APPLE_SEARCH_ADS_KEY_ID;
const PRIVATE_KEY = env.APPLE_SEARCH_ADS_PRIVATE_KEY;
const ORG_ID = 22309440;
function generateClientSecret() {
const header = { alg: "ES256", kid: KEY_ID, typ: "JWT" };
const now = Math.floor(Date.now() / 1000);
const payload = { sub: CLIENT_ID, aud: "https://appleid.apple.com", iat: now, exp: now + 86400 * 180, iss: TEAM_ID };
const base64Header = Buffer.from(JSON.stringify(header)).toString("base64url");
const base64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url");
const signingInput = `${base64Header}.${base64Payload}`;
const signer = crypto.createSign("SHA256");
signer.update(signingInput);
const signature = signer.sign({ key: PRIVATE_KEY.replace(/\\n/g, '\n'), dsaEncoding: 'ieee-p1363' }, "base64url");
return `${signingInput}.${signature}`;
}
async function getAccessToken(clientSecret) {
return new Promise((resolve, reject) => {
const postData = new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: clientSecret,
scope: 'searchadsorg'
}).toString();
const req = https.request({
hostname: 'appleid.apple.com',
port: 443,
path: '/auth/oauth2/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData),
'User-Agent': 'Mozilla/5.0'
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
function reqApi(path, token, method = 'GET', body = null) {
return new Promise((resolve) => {
const headers = {
'Authorization': `Bearer ${token}`,
'X-AP-Context': `orgId=${ORG_ID}`,
'User-Agent': 'Mozilla/5.0'
};
if (body) headers['Content-Type'] = 'application/json';
const req = https.request({
hostname: 'api.searchads.apple.com',
port: 443,
path,
method,
headers
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch { resolve({ raw: data, status: res.statusCode }); }
});
});
req.on('error', (e) => resolve({ error: e.message }));
if (body) req.write(JSON.stringify(body));
req.end();
});
}
async function run() {
const secret = generateClientSecret();
const token = (await getAccessToken(secret)).access_token;
console.log("Abfrage Kampagne 2144248407 Adgroups...");
const adgroups = await reqApi('/api/v5/campaigns/2144248407/adgroups', token);
console.log("Adgroups:", JSON.stringify(adgroups, null, 2));
const adgroupId = adgroups?.data?.[0]?.id;
if (adgroupId) {
console.log(`\nAbfrage Keywords für AdGroup ${adgroupId}...`);
const kwRes = await reqApi(`/api/v5/campaigns/2144248407/adgroups/${adgroupId}/targetingkeywords`, token);
console.log("Targeting Keywords:", JSON.stringify(kwRes, null, 2));
}
}
run();