import type { NullableId, Params, ServiceInterface } from '@feathersjs/feathers'; import type { Application } from '../../declarations'; import wildDuckClient from '../../clients/wildduck.client'; import { faker, th } from '@faker-js/faker'; import { BadRequest } from '@feathersjs/errors'; import config from 'config'; interface WildDuckAddress { success: boolean; id: string; address: string; main: boolean; user: string; tags: string[]; created: string; } interface GetWildDuckAddressInfoResponse { success: boolean; results: WildDuckAddress[]; } interface AliasApiResponse { id: string | null; address: string; tags: string[]; created: string; } interface CreateWildDuckAddressResponse { success: boolean; id: string; } type AliasesData = any; type AliasesPatch = any; type AliasesQuery = any; export type { WildDuckAddress as Aliases, AliasesData, AliasesPatch, AliasesQuery }; export interface AliasesServiceOptions { app: Application; } export interface AliasesParams extends Params { session?: any; } export class AliasesService implements ServiceInterface { constructor(public options: AliasesServiceOptions) {} async find(params: ServiceParams): Promise { const userId = await this.getUserIdByEmailAddress(params); return this.getUserAddresses(userId); } async create(data: AliasesData, params: ServiceParams): Promise; async create(data: AliasesData, params: ServiceParams): Promise { const userId = await this.getUserIdByEmailAddress(params); const randomString = faker.git.commitSha({ length: 4 }); // Replace all non-alphanumeric characters with nothing and spaces with dashes const alias = `${faker.color.human()}-${faker.animal.snake()}-${randomString}` .replace(/\s+/g, '-') .replace(/[^a-zA-Z0-9-]/g, '') .toLowerCase(); const emailDomain = config.get('wildDuck.domain'); const createResult = await wildDuckClient.post(`/users/${userId}/addresses`, { address: `${alias}@${emailDomain}`, }); if (!createResult.data.success) { throw new BadRequest('Failed to create alias'); } return this.getUserAddresses(userId); } private async getUserIdByEmailAddress(params: ServiceParams): Promise { const emails = params.session?.user?.emails; const preferredDomain = config.get('wildDuck.preferredDomain'); if (!emails.length || !preferredDomain) { throw new BadRequest('Unable to find user'); } const addressInfoResponse = await Promise.any( emails .filter((email: string) => email.endsWith(config.get('wildDuck.preferredDomain'))) .map((email: string) => wildDuckClient.get(`addresses/resolve/${email}`)) ); return addressInfoResponse.data.user; } private async getUserAddresses(userId: string): Promise { const { data: userAddressesResponse } = await wildDuckClient.get( `/users/${userId}/addresses` ); return userAddressesResponse.results.map(this.sanitizeAliasResponse); } async remove(id: NullableId, params: ServiceParams): Promise { const { data: addressInfoResponse } = await wildDuckClient.get(`addresses/resolve/${id}`); const allowedDomain: string = config.get('wildDuck.domain'); // If address does not match the allowed domain, throw an error if (!allowedDomain || !addressInfoResponse.address.endsWith(allowedDomain)) { throw new BadRequest('Unable to delete address'); } const userId = await this.getUserIdByEmailAddress(params); await wildDuckClient.delete(`users/${userId}/addresses/${id}`); return this.getUserAddresses(userId); } sanitizeAliasResponse(alias: WildDuckAddress): AliasApiResponse { // Prevent the user from deleting their main address or any address that does not end with the preferred domain const isRemovable = !alias.main && alias.address.endsWith(config.get('wildDuck.domain')); // Hide the id if the alias is not removable return { id: isRemovable ? alias.id : null, address: alias.address, tags: alias.tags, created: alias.created, }; } } export const getOptions = (app: Application) => { return { app }; };