|
import { Injectable } from '@nestjs/common'; |
|
import 'cookie-session'; |
|
import express from 'express'; |
|
import Cookies from 'cookies'; |
|
import { |
|
RequestContext, |
|
Customer, |
|
Order, |
|
OrderService, |
|
EntityNotFoundError, |
|
TransactionalConnection, |
|
SessionService, |
|
AuthOptions, |
|
ConfigService, |
|
isGraphQlErrorResult, |
|
} from '@vendure/core'; |
|
import { SortOrder } from '../codegen/generated-admin-types'; |
|
|
|
@Injectable() |
|
export class ImpersonateService { |
|
constructor( |
|
private configService: ConfigService, |
|
private orderService: OrderService, |
|
private sessionService: SessionService, |
|
private connection: TransactionalConnection, |
|
) {} |
|
|
|
private async getShopAPIContext(ctx: RequestContext) { |
|
return new RequestContext({ |
|
apiType: 'shop', |
|
channel: ctx.channel, |
|
isAuthorized: false, |
|
req: ctx.req, |
|
authorizedAsOwnerOnly: false, |
|
}); |
|
} |
|
|
|
/** |
|
* Sets the authToken either as a cookie or as a response header, depending on the |
|
* config settings. |
|
*/ |
|
private setSessionToken(options: { |
|
sessionToken: string; |
|
authOptions: Required<AuthOptions>; |
|
req: any; |
|
res: express.Response; |
|
}) { |
|
const { sessionToken, authOptions, req, res } = options; |
|
const name = authOptions.cookieOptions.name; |
|
const cookieName = typeof name === 'string' ? name : name.shop; |
|
const cookieSecret = authOptions.cookieOptions.secret; |
|
|
|
// Create cookies instance with the secret |
|
const cookies = new Cookies(req, res, { |
|
keys: [cookieSecret], |
|
}); |
|
|
|
// Create the session data object |
|
const sessionData = { token: sessionToken }; |
|
|
|
// Encode the data using base64 |
|
const encodedData = Buffer.from(JSON.stringify(sessionData)).toString('base64'); |
|
|
|
// Set the cookie with signature |
|
cookies.set(cookieName, encodedData, { |
|
httpOnly: true, |
|
signed: true, |
|
secure: req.secure, |
|
sameSite: 'strict', |
|
expires: new Date(Date.now() + 1000 * 60 * 60 * 8), // 8 hours |
|
overwrite: true, |
|
}); |
|
} |
|
|
|
async impersonateCustomer(ctx: RequestContext, customerId: string) { |
|
try { |
|
// Find the customer and their most recent order |
|
const customer = await this.connection.getRepository(ctx, Customer).findOne({ |
|
where: { id: customerId }, |
|
relations: ['user', 'user.roles', 'user.roles.channels', 'channels'], |
|
}); |
|
|
|
if (!customer) { |
|
throw new EntityNotFoundError('Customer', customerId); |
|
} |
|
|
|
// Find the most recent order for this customer |
|
const orders = await this.connection.getRepository(ctx, Order).find({ |
|
where: { customer }, |
|
order: { createdAt: SortOrder.DESC }, |
|
take: 1, |
|
relations: ['channels'], |
|
}); |
|
|
|
const mostRecentOrder = orders[0]; |
|
const shopCtx = await this.getShopAPIContext(ctx); |
|
|
|
// If the customer has a user account, create an authenticated session |
|
if (customer.user) { |
|
const session = await this.sessionService.createNewAuthenticatedSession( |
|
shopCtx, |
|
customer.user, |
|
'override', |
|
); |
|
if (isGraphQlErrorResult(session)) { |
|
return session; |
|
} |
|
this.setSessionToken({ |
|
authOptions: this.configService.authOptions, |
|
req: ctx.req, |
|
res: ctx.req.res, |
|
sessionToken: session.token, |
|
}); |
|
return session; |
|
} else if (mostRecentOrder) { |
|
// If no user account exists but there's an order, create an anonymous session |
|
const session = await this.sessionService.createAnonymousSession(); |
|
await this.sessionService.setActiveChannel(session, mostRecentOrder.channels[0]); |
|
// Associate the anonymous session with the customer's most recent order |
|
await this.orderService.addCustomerToOrder(ctx, mostRecentOrder.id, customer); |
|
|
|
this.setSessionToken({ |
|
authOptions: this.configService.authOptions, |
|
req: ctx.req, |
|
res: ctx.req.res, |
|
sessionToken: session.token, |
|
}); |
|
|
|
return session; |
|
} |
|
} catch (error) { |
|
if (error instanceof EntityNotFoundError) { |
|
throw error; |
|
} |
|
console.warn("Couldn't impersonate customer", error, error.stack); |
|
throw new Error(`Failed to impersonate customer: ${error.message}`); |
|
} |
|
} |
|
} |