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>
152 lines
4.3 KiB
JavaScript
152 lines
4.3 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;
|
|
|
|
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 options = {
|
|
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'
|
|
}
|
|
};
|
|
|
|
const req = https.request(options, (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 requestAppleApi(path, token, orgId, method = 'GET', body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const headers = {
|
|
'Authorization': `Bearer ${token}`,
|
|
'User-Agent': 'Mozilla/5.0'
|
|
};
|
|
if (orgId) {
|
|
headers['X-AP-Context'] = `orgId=${orgId}`;
|
|
}
|
|
if (body) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
const options = {
|
|
hostname: 'api.searchads.apple.com',
|
|
port: 443,
|
|
path: path,
|
|
method: method,
|
|
headers: headers
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
if (body) req.write(JSON.stringify(body));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const secret = generateClientSecret();
|
|
const tokenRes = await getAccessToken(secret);
|
|
const token = tokenRes.access_token;
|
|
|
|
console.log("Abfrage /api/v5/acls für orgId...");
|
|
const acls = await requestAppleApi('/api/v5/acls', token);
|
|
console.log("ACLs:", JSON.stringify(acls, null, 2));
|
|
|
|
const org = acls?.data?.[0];
|
|
const orgId = org?.orgId;
|
|
console.log("Verwende OrgId:", orgId);
|
|
|
|
if (orgId) {
|
|
// In Search Ads API v5: POST /api/v5/keywords/search
|
|
console.log("\nAbfrage Keywords via POST /api/v5/keywords/search ...");
|
|
const keywords = ['receipt scanner', 'receipt to excel', 'expense tracker', 'beleg scanner'];
|
|
const testKw = await requestAppleApi('/api/v5/keywords/search', token, orgId, 'POST', {
|
|
searchTerms: keywords,
|
|
countryOrRegion: 'US'
|
|
});
|
|
console.log("Keyword Search Result (US):", JSON.stringify(testKw, null, 2));
|
|
|
|
// Check DE
|
|
const testKwDE = await requestAppleApi('/api/v5/keywords/search', token, orgId, 'POST', {
|
|
searchTerms: ['beleg scanner', 'belege digitalisieren', 'kassenbon scanner', 'haushaltsbuch'],
|
|
countryOrRegion: 'DE'
|
|
});
|
|
console.log("Keyword Search Result (DE):", JSON.stringify(testKwDE, null, 2));
|
|
}
|
|
}
|
|
|
|
main();
|