Skip to content

Commit 33bfa13

Browse files
denbecclaude
andcommitted
Hardcode Google Wallet class ID, add venue/date, and upsert-class CLI
Remove GOOGLE_WALLET_CLASS_ID env var in favor of a hardcoded constant. Add venue (Bad Nauheim) and date/time to the Google Wallet class definition. Add upsert-class command to the test script for managing the class via the Google Wallet REST API instead of relying on JWT-based class creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 05b554b commit 33bfa13

3 files changed

Lines changed: 170 additions & 7 deletions

File tree

directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/test-wallet-passes.ts

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Run from the extension bundle root:
44
* npx tsx src/shared/__tests__/test-wallet-passes.ts
55
*/
6+
import { createSign } from 'node:crypto'
67
import fs from 'node:fs'
78
import path from 'node:path'
89
import { config } from 'dotenv'
@@ -23,8 +24,154 @@ const dummyTicket: WalletPassInput = {
2324
websiteUrl: 'https://programmier.bar',
2425
}
2526

27+
// --- Google Wallet REST API helpers ---
28+
29+
function base64url(input: string | Buffer): string {
30+
const buf = typeof input === 'string' ? Buffer.from(input) : input
31+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
32+
}
33+
34+
function createOAuthJwt(serviceAccountEmail: string, privateKey: string): string {
35+
const now = Math.floor(Date.now() / 1000)
36+
const header = { alg: 'RS256', typ: 'JWT' }
37+
const payload = {
38+
iss: serviceAccountEmail,
39+
scope: 'https://www.googleapis.com/auth/wallet_object.issuer',
40+
aud: 'https://oauth2.googleapis.com/token',
41+
iat: now,
42+
exp: now + 3600,
43+
}
44+
const encodedHeader = base64url(JSON.stringify(header))
45+
const encodedPayload = base64url(JSON.stringify(payload))
46+
const sign = createSign('RSA-SHA256')
47+
sign.update(`${encodedHeader}.${encodedPayload}`)
48+
const signature = base64url(sign.sign(privateKey))
49+
return `${encodedHeader}.${encodedPayload}.${signature}`
50+
}
51+
52+
async function getAccessToken(serviceAccountEmail: string, privateKey: string): Promise<string> {
53+
const jwt = createOAuthJwt(serviceAccountEmail, privateKey)
54+
const res = await fetch('https://oauth2.googleapis.com/token', {
55+
method: 'POST',
56+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
57+
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
58+
})
59+
const data = await res.json() as any
60+
if (!res.ok) {
61+
throw new Error(`OAuth failed: ${JSON.stringify(data)}`)
62+
}
63+
return data.access_token
64+
}
65+
66+
async function upsertGoogleWalletClass(env: Record<string, string>): Promise<void> {
67+
const issuerId = env.GOOGLE_WALLET_ISSUER_ID
68+
const email = env.GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL
69+
const privateKey = Buffer.from(env.GOOGLE_WALLET_PRIVATE_KEY_BASE64, 'base64').toString('utf-8')
70+
71+
// Must match the constant in wallet-pass-generator.ts
72+
const classSuffix = 'programmiercon_ticket_v4'
73+
const classId = `${issuerId}.${classSuffix}`
74+
75+
const classDefinition = {
76+
id: classId,
77+
issuerName: 'programmier.bar',
78+
logo: {
79+
sourceUri: {
80+
uri: `${dummyTicket.websiteUrl}/wallet_google_logo.png`,
81+
},
82+
},
83+
hexBackgroundColor: '#003F64',
84+
eventName: {
85+
defaultValue: {
86+
language: 'de',
87+
value: dummyTicket.conferenceTitle,
88+
},
89+
},
90+
venue: {
91+
name: {
92+
defaultValue: {
93+
language: 'de',
94+
value: 'Bad Nauheim',
95+
},
96+
},
97+
address: {
98+
defaultValue: {
99+
language: 'de',
100+
value: 'Bad Nauheim, Deutschland',
101+
},
102+
},
103+
},
104+
dateTime: {
105+
start: dummyTicket.conferenceDate,
106+
end: dummyTicket.conferenceEndDate,
107+
},
108+
reviewStatus: 'UNDER_REVIEW',
109+
}
110+
111+
console.log('Getting OAuth access token...')
112+
const accessToken = await getAccessToken(email, privateKey)
113+
114+
console.log(`Upserting class ${classId}...`)
115+
116+
// Try PUT (update) first, then POST (create) if 404
117+
const putRes = await fetch(
118+
`https://walletobjects.googleapis.com/walletobjects/v1/eventTicketClass/${classId}`,
119+
{
120+
method: 'PUT',
121+
headers: {
122+
Authorization: `Bearer ${accessToken}`,
123+
'Content-Type': 'application/json',
124+
},
125+
body: JSON.stringify(classDefinition),
126+
}
127+
)
128+
129+
if (putRes.ok) {
130+
console.log('Class updated successfully via REST API')
131+
const data = await putRes.json()
132+
console.log('Class data:', JSON.stringify(data, null, 2))
133+
return
134+
}
135+
136+
if (putRes.status === 404) {
137+
console.log('Class not found, creating...')
138+
const postRes = await fetch(
139+
'https://walletobjects.googleapis.com/walletobjects/v1/eventTicketClass',
140+
{
141+
method: 'POST',
142+
headers: {
143+
Authorization: `Bearer ${accessToken}`,
144+
'Content-Type': 'application/json',
145+
},
146+
body: JSON.stringify(classDefinition),
147+
}
148+
)
149+
if (postRes.ok) {
150+
console.log('Class created successfully via REST API')
151+
const data = await postRes.json()
152+
console.log('Class data:', JSON.stringify(data, null, 2))
153+
} else {
154+
const err = await postRes.text()
155+
console.error(`Failed to create class (${postRes.status}): ${err}`)
156+
}
157+
return
158+
}
159+
160+
const err = await putRes.text()
161+
console.error(`Failed to update class (${putRes.status}): ${err}`)
162+
}
163+
164+
// --- Main ---
165+
26166
async function main() {
27167
const env = process.env as Record<string, string>
168+
const command = process.argv[2]
169+
170+
// If called with "upsert-class", just create/update the Google class via REST API
171+
if (command === 'upsert-class') {
172+
await upsertGoogleWalletClass(env)
173+
return
174+
}
28175

29176
console.log('\n--- Apple Wallet ---')
30177
const hasAppleConfig = !!(
@@ -57,7 +204,6 @@ async function main() {
57204
console.log('\n--- Google Wallet ---')
58205
const hasGoogleConfig = !!(
59206
env.GOOGLE_WALLET_ISSUER_ID &&
60-
env.GOOGLE_WALLET_CLASS_ID &&
61207
env.GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL &&
62208
env.GOOGLE_WALLET_PRIVATE_KEY_BASE64
63209
)
@@ -90,7 +236,7 @@ async function main() {
90236
' Apple: APPLE_WALLET_PASS_TYPE_ID, APPLE_WALLET_TEAM_ID, APPLE_WALLET_SIGNER_CERT_BASE64, APPLE_WALLET_SIGNER_KEY_BASE64, APPLE_WALLET_WWDR_BASE64'
91237
)
92238
console.log(
93-
' Google: GOOGLE_WALLET_ISSUER_ID, GOOGLE_WALLET_CLASS_ID, GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL, GOOGLE_WALLET_PRIVATE_KEY_BASE64'
239+
' Google: GOOGLE_WALLET_ISSUER_ID, GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL, GOOGLE_WALLET_PRIVATE_KEY_BASE64'
94240
)
95241
}
96242
}

directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/wallet-pass-generator.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ interface AppleWalletConfig {
2222
wwdr: string | Buffer
2323
}
2424

25+
const GOOGLE_WALLET_CLASS_SUFFIX = 'programmiercon_ticket_v4'
26+
2527
interface GoogleWalletConfig {
2628
issuerId: string
27-
classId: string
2829
serviceAccountEmail: string
2930
privateKey: string
3031
}
@@ -168,17 +169,15 @@ export async function generateAppleWalletPass(
168169

169170
function loadGoogleConfig(env: Record<string, string>): GoogleWalletConfig | null {
170171
const issuerId = env.GOOGLE_WALLET_ISSUER_ID
171-
const classId = env.GOOGLE_WALLET_CLASS_ID
172172
const email = env.GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL
173173
const keyBase64 = env.GOOGLE_WALLET_PRIVATE_KEY_BASE64
174174

175-
if (!issuerId || !classId || !email || !keyBase64) {
175+
if (!issuerId || !email || !keyBase64) {
176176
return null
177177
}
178178

179179
return {
180180
issuerId,
181-
classId,
182181
serviceAccountEmail: email,
183182
privateKey: Buffer.from(keyBase64, 'base64').toString('utf-8'),
184183
}
@@ -211,7 +210,7 @@ export function generateGoogleWalletUrl(
211210

212211
const verifyUrl = `${input.websiteUrl}/ticket/${input.ticketCode}`
213212
const objectId = `${config.issuerId}.${input.ticketCode}`
214-
const classId = `${config.issuerId}.${config.classId}`
213+
const classId = `${config.issuerId}.${GOOGLE_WALLET_CLASS_SUFFIX}`
215214

216215
const eventTicketObject = {
217216
id: objectId,
@@ -262,6 +261,24 @@ export function generateGoogleWalletUrl(
262261
value: input.conferenceTitle,
263262
},
264263
},
264+
venue: {
265+
name: {
266+
defaultValue: {
267+
language: 'de',
268+
value: 'Bad Nauheim',
269+
},
270+
},
271+
address: {
272+
defaultValue: {
273+
language: 'de',
274+
value: 'Bad Nauheim, Deutschland',
275+
},
276+
},
277+
},
278+
dateTime: {
279+
start: input.conferenceDate,
280+
...(input.conferenceEndDate && { end: input.conferenceEndDate }),
281+
},
265282
reviewStatus: 'UNDER_REVIEW',
266283
}
267284

0 commit comments

Comments
 (0)