UNPKG

@brandcast_app/zoomshift-api-client

Version:

Unofficial TypeScript/JavaScript client for ZoomShift employee scheduling API (reverse-engineered)

309 lines (308 loc) 11.5 kB
"use strict"; /** * ZoomShift API Client * * Unofficial TypeScript client for ZoomShift employee scheduling. * Based on reverse-engineering ZoomShift's internal JSON endpoints. * * @license MIT * @see https://github.com/jduncan-rva/zoomshift-api-client */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZoomShiftClient = void 0; const axios_1 = __importDefault(require("axios")); const axios_cookiejar_support_1 = require("axios-cookiejar-support"); const tough_cookie_1 = require("tough-cookie"); const jsdom_1 = require("jsdom"); /** * Main ZoomShift API client class * * @example * ```typescript * const client = new ZoomShiftClient({ debug: true }); * * // Authenticate * await client.authenticate('user@example.com', 'password', '58369'); * * // Get shifts * const shifts = await client.getShifts({ * startDate: '2025-10-10', * endDate: '2025-10-17' * }); * ``` */ class ZoomShiftClient { constructor(options = {}) { this.authenticated = false; this.scheduleId = null; this.debug = options.debug || false; this.cookieJar = new tough_cookie_1.CookieJar(); this.client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ baseURL: ZoomShiftClient.BASE_URL, timeout: options.timeout || 30000, jar: this.cookieJar, withCredentials: true, headers: { 'User-Agent': options.userAgent || 'BrandCast ZoomShift Client/1.0', 'Accept': 'application/json, text/html', }, })); // Debug logging interceptors if (this.debug) { this.client.interceptors.request.use((config) => { console.log('[ZoomShift] Request:', config.method?.toUpperCase(), config.url); return config; }); this.client.interceptors.response.use((response) => { console.log('[ZoomShift] Response:', response.status, response.config.url); return response; }, (error) => { console.error('[ZoomShift] Error:', error.message); return Promise.reject(error); }); } } /** * Authenticate with ZoomShift * * @param email - ZoomShift account email * @param password - ZoomShift account password * @param scheduleId - ZoomShift schedule ID (found in URL) * @returns Authentication response with schedule info * @throws {ZoomShiftError} If authentication fails * * @example * ```typescript * const auth = await client.authenticate( * 'manager@example.com', * 'mypassword', * '58369' * ); * console.log('Authenticated:', auth.authenticated); * ``` */ async authenticate(email, password, scheduleId) { try { // Step 1: Get login page and extract CSRF token if (this.debug) { console.log('[ZoomShift] Fetching login page...'); } const loginPage = await this.client.get('/session/new'); const dom = new jsdom_1.JSDOM(loginPage.data); const csrfToken = dom.window.document .querySelector('meta[name="csrf-token"]') ?.getAttribute('content'); if (!csrfToken) { throw this.createError('CSRF_TOKEN_MISSING', 'Failed to extract CSRF token from login page'); } if (this.debug) { console.log('[ZoomShift] CSRF token extracted'); } // Step 2: Submit login form const formData = new URLSearchParams({ 'authenticity_token': csrfToken, 'session[email]': email, 'session[password]': password, }); if (this.debug) { console.log('[ZoomShift] Submitting login form...'); } const loginResponse = await this.client.post('/session', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, maxRedirects: 0, validateStatus: (status) => status >= 200 && status < 400, }); // Step 3: Check if authentication succeeded // Successful auth redirects away from /session/new const finalUrl = loginResponse.request.path || loginResponse.request.res?.responseUrl || ''; if (finalUrl.includes('/session/new')) { throw this.createError('AUTH_FAILED', 'Authentication failed - invalid credentials'); } this.authenticated = true; this.scheduleId = scheduleId; if (this.debug) { console.log('[ZoomShift] Authentication successful'); } return { authenticated: true, scheduleId, userName: email, }; } catch (error) { this.authenticated = false; if (this.isZoomShiftError(error)) { throw error; } if (axios_1.default.isAxiosError(error)) { throw this.createError('AUTH_REQUEST_FAILED', `Authentication request failed: ${error.message}`, error); } throw this.createError('AUTH_UNKNOWN_ERROR', 'Unknown authentication error occurred', error); } } /** * Get shifts for a date range * * @param request - Request parameters (date range, filters) * @returns Array of shift objects * @throws {ZoomShiftError} If not authenticated or request fails * * @example * ```typescript * const shifts = await client.getShifts({ * startDate: '2025-10-10', * endDate: '2025-10-17', * employeeId: '12345', // optional * location: 'Main Store' // optional * }); * ``` */ async getShifts(request) { if (!this.authenticated || !this.scheduleId) { throw this.createError('NOT_AUTHENTICATED', 'Not authenticated - call authenticate() first'); } try { if (this.debug) { console.log('[ZoomShift] Fetching shifts:', request); } const response = await this.client.get(`/${this.scheduleId}/shifts.json`, { params: { page: 1, limit: 101, pagination_adjustment: 0, order: 'timeframe', sort: 'asc', rendition: 'calendar', start_date: request.startDate, end_date: request.endDate, variation: 'all', }, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, }); // Transform ZoomShift API response to our interface const shifts = this.transformShifts(response.data, request); if (this.debug) { console.log('[ZoomShift] Fetched', shifts.length, 'shifts'); } return shifts; } catch (error) { // Re-authenticate on 401 if (axios_1.default.isAxiosError(error) && error.response?.status === 401) { this.authenticated = false; throw this.createError('SESSION_EXPIRED', 'Session expired - please re-authenticate'); } if (axios_1.default.isAxiosError(error)) { throw this.createError('FETCH_FAILED', `Failed to fetch shifts: ${error.message}`, error); } throw this.createError('FETCH_UNKNOWN_ERROR', 'Unknown error while fetching shifts', error); } } /** * Transform ZoomShift API response to standardized format * ZoomShift uses JSON:API format with data/attributes structure */ transformShifts(rawData, request) { const shifts = []; // ZoomShift API returns JSON:API format: { data: [...], meta: {...} } if (!rawData || !Array.isArray(rawData.data)) { if (this.debug) { console.warn('[ZoomShift] Unexpected response structure:', rawData); } return shifts; } for (const shift of rawData.data) { if (shift.type !== 'shift' || !shift.attributes) { continue; } const attrs = shift.attributes; const assignment = attrs.assignment_attributes || {}; // Build employee name from first/last name const employeeName = [assignment.first_name, assignment.last_name] .filter(Boolean) .join(' ') || 'Unknown Employee'; const employeeId = assignment.user_id?.toString() || ''; // Filter by employeeId if specified if (request.employeeId && employeeId !== request.employeeId) { continue; } // Filter by location if specified if (request.location && attrs.location_name !== request.location) { continue; } // Convert duration from seconds to hours const durationHours = attrs.duration ? attrs.duration / 3600 : 0; const breakHours = attrs.break_time ? attrs.break_time / 3600 : undefined; shifts.push({ id: shift.id, employeeName, employeeId, role: attrs.position_name || 'Employee', startTime: attrs.start_at, endTime: attrs.end_at, duration: durationHours, breakDuration: breakHours, location: attrs.location_name || undefined, notes: attrs.note?.all || undefined, status: this.mapStatus(attrs.published, attrs.confirmed), }); } return shifts; } /** * Map ZoomShift status flags to our standard status * * ZoomShift doesn't have explicit status field, but we can infer from: * - published: boolean - shift is published to schedule * - confirmed: boolean - shift is confirmed by employee * - start_at/end_at timestamps - can determine if in progress or completed */ mapStatus(published, confirmed) { // For now, return 'scheduled' for all published shifts // Future enhancement: check timestamps to determine in_progress/completed return 'scheduled'; } /** * Check if client is authenticated */ isAuthenticated() { return this.authenticated; } /** * Get the schedule ID */ getScheduleId() { return this.scheduleId; } /** * Create a standardized error object */ createError(code, message, details) { return { code, message, details, }; } /** * Type guard for ZoomShiftError */ isZoomShiftError(error) { return (typeof error === 'object' && error !== null && 'code' in error && 'message' in error); } } exports.ZoomShiftClient = ZoomShiftClient; ZoomShiftClient.BASE_URL = 'https://app.zoomshift.com';