Skip to content

Instantly share code, notes, and snippets.

@taxilian
Created November 5, 2024 03:17
Show Gist options
  • Save taxilian/f966ac0964c41b6860b9d897e183c58a to your computer and use it in GitHub Desktop.
Save taxilian/f966ac0964c41b6860b9d897e183c58a to your computer and use it in GitHub Desktop.
A relatively simple way to handle impersonating a customer in vendure

Assumptions

This assumes that you're using standard auth strategies, you have set a cookie secret, and you're using a separate cookie for store and admin APIs, e.g.

  authOptions: {
    tokenMethod: ['cookie'],
    cookieOptions: {
      name: { shop: 'vendshop', admin: 'vendadmin' },
      secret: 'somethingthatIwillnevertellyouandyouwillneverguessthisisntitipromisenoreally',
    },
  }

It's not perfect, but it sets the cookies so that you're logged in as a customer. There are probably bugs and crap, but I'm still working through stuff.

import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { gql } from 'graphql-tag';
import { ImpersonateResolver } from './impersonate.resolver';
import { ImpersonateService } from './impersonate.service';
import path from 'path';
import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
const schemaExtension = gql`
extend type Mutation {
impersonateCustomer(customerId: ID!): ImpersonateCustomerResult!
}
type ImpersonateCustomerResult {
token: String!
success: Boolean!
}
`;
@VendurePlugin({
imports: [PluginCommonModule],
providers: [ImpersonateService],
adminApiExtensions: {
schema: schemaExtension,
resolvers: [ImpersonateResolver],
},
})
export class ImpersonatePlugin {
static uiExtensions: AdminUiExtension = {
extensionPath: path.join(__dirname, 'ui'),
providers: ['providers.ts'],
};
}
// src/plugins/impersonate/impersonate.resolver.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
import { ImpersonateService } from './impersonate.service';
import { Response } from 'express';
@Resolver()
export class ImpersonateResolver {
constructor(private impersonateService: ImpersonateService) {}
@Mutation()
@Allow(Permission.SuperAdmin)
async impersonateCustomer(@Ctx() ctx: RequestContext, @Args() args: { customerId: string }) {
const session = await this.impersonateService.impersonateCustomer(ctx, args.customerId);
if (session) {
return {
token: session.token,
success: true,
};
} else {
return {
token: '',
success: false,
};
}
}
}
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}`);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment