import { ProfileDataExtended, ProfileOptions, ProfilePictureResponse, ProfilePictureSize, ProfilePictureUploadResponse, RegisterFormData, SelfRegisterFormData, } from '../../interfaces/user.interface' import { Api, ApiResponse } from '../api' import { ENDPOINTS } from '.' export const SELF_REGISTRATION_REQUEST_TYPE = 'SelfRegistrationRequest' export const SELF_REGISTRATION_CONFIG_TYPE = 'SelfRegistrationConfiguration' export class Registration { api: Api constructor(api: Api) { this.api = api } async getMyProfile(argOptions?: ProfileOptions) { const options = Object.assign( { useCache: true, autoLogin: false }, argOptions, ) const user: ApiResponse = await this.api.apisauce.get( `${ENDPOINTS.REGISTRATION}/users/my-profile`, undefined, { headers: { ['x-autologin']: options.autoLogin ? 'true' : undefined, }, cache: options.useCache ? undefined : false, }, ) if ( !user.ok || !user.data || user.headers?.['content-type'].indexOf('text/html') !== -1 ) { throw new Error('Not logged in') } return user.data } async updateRecentOrganizations( recentUserOrganization: string, argOptions?: ProfileOptions, ) { const options = Object.assign( { useCache: true, autoLogin: false }, argOptions, ) const response = await this.api.apisauce.put( `${ENDPOINTS.REGISTRATION}/users/metadata`, { recentUserOrganization, }, { headers: { ['x-autologin']: options.autoLogin ? 'true' : undefined, }, cache: options.useCache ? undefined : false, }, ) if (!response.ok) { throw new Error('Unable to update user metadata') } } async register(data?: RegisterFormData) { return this.api.apisauce.post<{ message: string }>( `${ENDPOINTS.REGISTRATION}/create`, data, ) } async verifyMail(code: string) { return this.api.apisauce.post( `${ENDPOINTS.REGISTRATION}/confirm/${code}`, ) } async requestRegisterWithConfiguration( configurationHash: string, email: string, ) { const data = { email, } return this.api.apisauce.post<{ message: string }>( `${ENDPOINTS.REGISTRATION}/create/${configurationHash}`, data, { timeout: 30000, }, ) } async verify(hash: string, data?: SelfRegisterFormData) { return this.api.apisauce.post<{ message: string }>( `${ENDPOINTS.REGISTRATION}/verify/${hash}`, data, { timeout: 30000, }, ) } /** * Uploads a profile picture for the current user by sending RAW binary * image data. API Gateway will base64-encode it and set isBase64Encoded * for the Lambda handler. * * Accepts Blobs/Buffers/ArrayBuffers/Uint8Arrays or Data URLs/base64 strings. * * Size limit: <= 3.5MB (decoded/binary). */ async uploadProfilePicture( file: | Blob | ArrayBuffer | Uint8Array | Buffer | string /* data URL or base64 string */, contentType?: string, ) { const normalized = await normalizeImagePayload(file, contentType) // Axios (via apisauce) will send raw body when given Buffer/Blob/ArrayBuffer/Uint8Array return this.api.apisauce.post( `${ENDPOINTS.PROFILE_PICTURES}/upload`, normalized.body, { headers: { 'Content-Type': normalized.contentType, }, // prevent any JSON transforms if present in global config transformRequest: [(data) => data], }, ) } /** * Retrieves a profile picture for a specific user and size * * @param uid - User ID whose profile picture to retrieve * @param size - Image size variant (thumb, medium, large) * @returns Promise resolving to profile picture response with signed URL * * @example * ```typescript * // Get medium size profile picture for user * const result = await registration.getProfilePicture('user123', 'medium') * console.log('Image URL:', result.url) * * // Get thumbnail for current user * const result = await registration.getProfilePicture('current', 'thumb') * ``` */ async getProfilePicture(uid: string, size: ProfilePictureSize = 'medium') { return this.api.apisauce.get( `${ENDPOINTS.PROFILE_PICTURES}/${uid}/${size}`, ) } /** * Deletes the current user's profile pictures * * @returns Promise resolving when profile pictures are successfully deleted * * @example * ```typescript * await registration.deleteProfilePicture() * console.log('Profile pictures deleted') * ``` */ async deleteProfilePicture() { return this.api.apisauce.delete( `${ENDPOINTS.PROFILE_PICTURES}/delete`, ) } } function isNodeBuffer(value: unknown): value is Buffer { // Guard to avoid importing Buffer types outside Node return ( typeof Buffer !== 'undefined' && value instanceof Buffer && typeof (value as Buffer).byteLength === 'number' ) } async function normalizeImagePayload( input: Blob | ArrayBuffer | Uint8Array | Buffer | string, contentType?: string, ): Promise<{ body: Blob | ArrayBuffer | Uint8Array | Buffer contentType: string }> { // If Blob: trust provided contentType or infer from Blob if (typeof Blob !== 'undefined' && input instanceof Blob) { const type = contentType || input.type || 'application/octet-stream' // Enforce 3.5MB size if (input.size > 3.5 * 1024 * 1024) { throw new Error('File too large. Maximum size is 3.5MB') } return { body: input, contentType: type } } // If Node Buffer if (isNodeBuffer(input)) { if (input.byteLength > 3.5 * 1024 * 1024) { throw new Error('File too large. Maximum size is 3.5MB') } return { body: input, contentType: contentType || 'application/octet-stream', } } // If ArrayBuffer/Uint8Array if (input instanceof ArrayBuffer || input instanceof Uint8Array) { const size = input instanceof ArrayBuffer ? input.byteLength : input.byteLength if (size > 3.5 * 1024 * 1024) { throw new Error('File too large. Maximum size is 3.5MB') } return { body: input, contentType: contentType || 'application/octet-stream', } } // If string: handle data URL or base64 string if (typeof input === 'string') { const dataUrlMatch = input.match(/^data:([^;]+);base64,(.*)$/) if (dataUrlMatch) { const [, inferredType, b64] = dataUrlMatch const bytes = base64ToBytes(b64) if (bytes.byteLength > 3.5 * 1024 * 1024) { throw new Error('File too large. Maximum size is 3.5MB') } const type = contentType || inferredType || 'application/octet-stream' if (typeof Blob !== 'undefined') { return { body: new Blob([bytes], { type }), contentType: type } } return { body: bytes, contentType: type } } // treat as raw base64 without data URL prefix const bytes = base64ToBytes(input) if (bytes.byteLength > 3.5 * 1024 * 1024) { throw new Error('File too large. Maximum size is 3.5MB') } const type = contentType || 'application/octet-stream' if (typeof Blob !== 'undefined') { return { body: new Blob([bytes], { type }), contentType: type } } return { body: bytes, contentType: type } } throw new Error('Unsupported image input type') } function base64ToBytes(b64: string): Uint8Array { if (typeof atob === 'function') { // browser const binary = atob(b64) const len = binary.length const bytes = new Uint8Array(len) for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i) return bytes } else if (typeof Buffer !== 'undefined') { // node return new Uint8Array(Buffer.from(b64, 'base64')) } throw new Error('Base64 decode not supported in this environment') }