Spaces:
Sleeping
Sleeping
| /** | |
| * Android Management API (AMAPI) service — port of amapi_service.py using the | |
| * Node `googleapis` client. Used for Google Cloud DPC QR provisioning and | |
| * enterprise/device administration. | |
| */ | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| import { google } from 'googleapis'; | |
| import { settings } from '../config.js'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const SCOPES = ['https://www.googleapis.com/auth/androidmanagement']; | |
| const CLOUD_DPC_COMPONENT = 'com.google.android.apps.work.clouddpc/.receivers.CloudDeviceAdminReceiver'; | |
| const CLOUD_DPC_SIGNATURE = 'I5YvS0O5hXY46mb01BlRjq4oJJGs2kuUcHvVkAPEXlg'; | |
| const CLOUD_DPC_DOWNLOAD = 'https://play.google.com/managed/downloadManagingApp?identifier=setup'; | |
| function localServiceAccountPaths() { | |
| return [ | |
| path.resolve(__dirname, '..', '..', 'service-account.json'), | |
| path.resolve(__dirname, '..', '..', '..', 'backend', 'service-account.json'), | |
| 'service-account.json', | |
| ]; | |
| } | |
| function loadCredentials() { | |
| const credValue = settings.AMAPI_SERVICE_ACCOUNT_JSON; | |
| for (const p of localServiceAccountPaths()) { | |
| if (fs.existsSync(p)) { | |
| return JSON.parse(fs.readFileSync(p, 'utf8')); | |
| } | |
| } | |
| if (!credValue) { | |
| throw new Error('AMAPI_SERVICE_ACCOUNT_JSON not configured.'); | |
| } | |
| if (fs.existsSync(credValue)) { | |
| return JSON.parse(fs.readFileSync(credValue, 'utf8')); | |
| } | |
| // base64-encoded JSON | |
| try { | |
| return JSON.parse(Buffer.from(credValue, 'base64').toString('utf8')); | |
| } catch { /* fall through */ } | |
| // raw JSON | |
| try { | |
| return JSON.parse(credValue); | |
| } catch { | |
| throw new Error('AMAPI_SERVICE_ACCOUNT_JSON is not a valid file path, base64 JSON, or raw JSON.'); | |
| } | |
| } | |
| async function getService() { | |
| const credentials = loadCredentials(); | |
| const auth = new google.auth.GoogleAuth({ credentials, scopes: SCOPES }); | |
| const authClient = await auth.getClient(); | |
| return google.androidmanagement({ version: 'v1', auth: authClient }); | |
| } | |
| export function isConfigured() { | |
| if (settings.AMAPI_SERVICE_ACCOUNT_JSON && settings.AMAPI_PROJECT_ID) return true; | |
| if (settings.AMAPI_PROJECT_ID) { | |
| return localServiceAccountPaths().some((p) => fs.existsSync(p)); | |
| } | |
| return false; | |
| } | |
| export function getEnterpriseName() { | |
| return settings.AMAPI_ENTERPRISE_NAME || null; | |
| } | |
| export async function createEnterprise() { | |
| const service = await getService(); | |
| const { data } = await service.enterprises.create({ | |
| projectId: settings.AMAPI_PROJECT_ID, | |
| agreementAccepted: true, | |
| requestBody: { enterpriseDisplayName: 'RR Locker EMI Finance' }, | |
| }); | |
| return { enterprise_name: data.name || '', enterprise: data }; | |
| } | |
| export async function createPolicy(enterpriseName, policyName = 'emi-locker-policy') { | |
| const service = await getService(); | |
| const policy = { | |
| advancedSecurityOverrides: { | |
| untrustedAppsPolicy: 'ALLOW_INSTALL_DEVICE_WIDE', | |
| googlePlayProtectVerifyApps: 'VERIFY_APPS_USER_CHOICE', | |
| developerSettings: 'DEVELOPER_SETTINGS_ALLOWED', | |
| }, | |
| factoryResetDisabled: true, | |
| safeBootDisabled: true, | |
| screenCaptureDisabled: true, | |
| addUserDisabled: true, | |
| removeUserDisabled: true, | |
| modifyAccountsDisabled: false, | |
| systemUpdate: { type: 'WINDOWED', startMinutes: 120, endMinutes: 300 }, | |
| skipFirstUseHintsEnabled: true, | |
| adjustVolumeDisabled: false, | |
| funDisabled: true, | |
| networkEscapeHatchEnabled: true, | |
| playStoreMode: 'BLACKLIST', | |
| }; | |
| const { data } = await service.enterprises.policies.patch({ | |
| name: `${enterpriseName}/policies/${policyName}`, | |
| requestBody: policy, | |
| }); | |
| return data; | |
| } | |
| export async function createEnrollmentToken(enterpriseName, policyName = 'emi-locker-policy') { | |
| const service = await getService(); | |
| const { data } = await service.enterprises.enrollmentTokens.create({ | |
| parent: enterpriseName, | |
| requestBody: { | |
| policyName: `${enterpriseName}/policies/${policyName}`, | |
| duration: '86400s', | |
| allowPersonalUsage: 'PERSONAL_USAGE_DISALLOWED', | |
| oneTimeOnly: false, | |
| }, | |
| }); | |
| return data; | |
| } | |
| export function generateQrPayload(tokenValue) { | |
| return { | |
| 'android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME': CLOUD_DPC_COMPONENT, | |
| 'android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM': CLOUD_DPC_SIGNATURE, | |
| 'android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION': CLOUD_DPC_DOWNLOAD, | |
| 'android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE': { | |
| 'com.google.android.apps.work.clouddpc.EXTRA_ENROLLMENT_TOKEN': tokenValue, | |
| }, | |
| 'android.app.extra.PROVISIONING_SKIP_ENCRYPTION': true, | |
| 'android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED': true, | |
| }; | |
| } | |
| export function generateQrString(tokenValue) { | |
| return JSON.stringify(generateQrPayload(tokenValue)); | |
| } | |
| export async function listDevices(enterpriseName) { | |
| const service = await getService(); | |
| const { data } = await service.enterprises.devices.list({ parent: enterpriseName }); | |
| return data.devices || []; | |
| } | |
| export async function deleteDevice(deviceName) { | |
| const service = await getService(); | |
| try { | |
| await service.enterprises.devices.delete({ name: deviceName }); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| export async function cleanupAllDevices(enterpriseName) { | |
| const devices = await listDevices(enterpriseName); | |
| let deleted = 0; | |
| let failed = 0; | |
| for (const device of devices) { | |
| if (device.name) { | |
| // eslint-disable-next-line no-await-in-loop | |
| if (await deleteDevice(device.name)) deleted += 1; else failed += 1; | |
| } | |
| } | |
| return { total: devices.length, deleted, failed }; | |
| } | |
| export async function createNewEnterprise() { | |
| const service = await getService(); | |
| const { data } = await service.enterprises.create({ | |
| projectId: settings.AMAPI_PROJECT_ID, | |
| agreementAccepted: true, | |
| requestBody: { enterpriseDisplayName: 'RR Locker EMI Finance' }, | |
| }); | |
| const newName = data.name || ''; | |
| settings.AMAPI_ENTERPRISE_NAME = newName; | |
| try { | |
| await createPolicy(newName); | |
| } catch { /* best effort */ } | |
| return { enterprise_name: newName, enterprise: data }; | |
| } | |
| export default { | |
| isConfigured, | |
| getEnterpriseName, | |
| createEnterprise, | |
| createPolicy, | |
| createEnrollmentToken, | |
| generateQrPayload, | |
| generateQrString, | |
| listDevices, | |
| deleteDevice, | |
| cleanupAllDevices, | |
| createNewEnterprise, | |
| }; | |