Goal
I'm attempting to build a multi-tenant application with Feathers JS. On login, the tenant ID will be included in the request. From then on, the tenant ID will be gotten from the user field with the request parameters. Every user has this tenantId field.
In MongoDB, I have one unique collection of every data type per tenant. The collection names look like tenantId.documents and tenantId.users
Problem
The service generated via the feathers generate service CLI command looks like so:
export class Documents extends Service {
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
const client: Promise<Db> = app.get('mongoClient');
client.then(db => {
this.Model = db.collection('documents');
});
}
}
As you can see, the generated Services seem to need their collection name ("documents" in this case) during instantiation. Normally, this makes sense since it saves time awaiting a call to app.get("mongoClient")
However, since I need to dynamically change which collection I read from based on the User's tenantId, this won't work for me.
I implemented something like the following:
export class Documents extends Service {
client: Promise<Db>
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
this.client = app.get("mongoClient");
}
async create(data: IDocumentData, params: Params) {
const db: Db = await this.client;
this.Model = db.collection(`${params.user!!.organizationId}.documents`);
return super.create(data, params);
}
}
The problems are these:
I need to await this.client every request, even when the promise will probably already be fulfilled by the time a user actually makes a request to this service
I have to implement every method of the parent Service even though I barely need to add any real functionality.
Question
What is the most feathers-y way to solve this problem?
I don't want to override every method that I need in every service
I don't see a way to handle this with middleware or hooks.
I also don't think it's necessary to create one service instance per tenant in my application. It seems wasteful since I don't need to make any additional external requests based on the tenant ID, I just need to change the collection
Is there a good, pretty way to do this in Feathers?
Thanks to the helpful Feather community Slack channel, I think I came across a halfway-decent solution to this specific issue. It doesn't address all of my concerns, but at least it de-clutters my code.
First, I should create a new class that extends the built in Service class that implements the feature that I want. It could look something like this:
class DynamicMongoService extends Service {
client: Promise<Db>;
collectionName: string;
constructor(
options: Partial<MongoDBServiceOptions>,
app: Application,
collectionName: string
) {
super(options);
this.client = app.get("mongoClient");
this.collectionName = collectionName;
}
async getCollection(params: Params) {
const db: Db = await this.client;
this.Model = db.collection(
`${params!!.user!!.organizationId}.${this.collectionName}`
);
}
async find(params?: Params) {
await this.getCollection(params!!);
return super.create(params!!);
}
async get(id: Id, params?: Params) {
await this.getCollection(params!!);
return super.get(id, params);
}
async create(data: Partial<any> | Array<Partial<any>>, params?: Params) {
await this.getCollection(params!!);
return super.create(data, params);
}
async update(id: NullableId, data: any, params?: Params) {
await this.getCollection(params!!);
return super.update(id!!, data, params);
}
async patch(id: NullableId, data: Partial<any>, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, data, params);
}
async remove(id: NullableId, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, params!!);
}
}
The key elements are thus:
Pass collection name in the constructor
Get the collection name before each method
An implementation of this service would look like this:
export class Documents extends DynamicMongoService {
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options, app, "documents");
}
}
Not the best, but easy enough!
Related
dI am currently trying to implement a stripe integration for our client-side to start checkout processes, manage subscriptions, etc... Altough the documentation seems pretty straight forward i have followed the steps for implementing the node SDK, but it does not work for whatever reason.
After calling a method (e.g list all customers) i dont see any request going out in the network tab, there is also no error it just seems that it does nothing at all. I have already tried using a different apiVersion, nothing changed.
This is what the shell class looks like:
import { StripeConfig } from 'shared/src/sharedConfigs/configs/stripeConfig';
import Stripe from 'stripe';
export class StripeShell {
private readonly publishableKey: string;
private readonly secretKey: string;
private readonly apiVersion: string;
private stripeApi: Stripe;
public constructor(config: StripeConfig) {
this.publishableKey = config.publishableKey;
this.secretKey = config.secretKey;
this.apiVersion = config.apiVersion;
this.stripeApi = new Stripe(this.secretKey, {
// #ts-ignore
apiVersion: '2020-08-01',
});
}
public getCustomer(customerId: string): Promise<Stripe.Customer | Stripe.DeletedCustomer> {
return this.stripeApi.customers.retrieve(customerId);
}
public listCustomers(): Promise<Stripe.ApiList<Stripe.Customer>> {
return this.stripeApi.customers.list();
}
public getSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
return this.stripeApi.subscriptions.retrieve(subscriptionId);
}
public getInvoice(invoiceId: string): Promise<Stripe.Invoice> {
return this.stripeApi.invoices.retrieve(invoiceId);
}
}
And this is how i tried testing it:
const test = () => {
shell.stripe.listCustomers().then((res) => {
console.log(res);
}).catch(err => console.error(err));
};
I am using React as frontend Framework and Typescript.
I already tried requiring stripe instead of importing, also tried different apiVersions, also tried using curl which works fine, double checked the secret/publishable key and tried both.
I am working with a system which's 2 years old already using a layered approach (controllers, services, models, repos).
I have a set of requirements for sending several welcome/onbording emails based on actions on the system, specially on user-signup and user-password-reset-requested.
In the current implementation from the older team they do the following:
Signup the user
Send a basic welcome email, the same for everybody
That approach is simple, now I need to check for several fields in different entities to discover which email has to be sent. For me the service above is likely violating the SRP (single responsability pattern) and maybe the task of sending emails looks like more a side-effect. This is my current approach:
Signed the user, Blank the user password on reset
Trigger to SQS an user-signed-up and user-password-reset-requested events
Listen the event and process the email
I feel like adding this extra EDA (event-driven architecture) idea, brings the benefit of decoupling the app service, and reliability, since if the email failed, the user was created/resetted but no email was sent. Since I may retry on errors I will not loose any email, in fact I used the outbox pattern to make it more transactional to the user change.
export class UserService {
public async blankUserPasswordByEmail(email: string): Promise<void> {
await this.getManager().transaction(async (entityManager) => {
const userRepo = entityManager.getCustomRepository(UserRepository);
const outboxRepo = entityManager.getCustomRepository(OutboxRepository);
const user = await userRepo.findByEmail(email);
const outboxMessage = new Outbox();
outboxMessage.entityId = user.id;
outboxMessage.entityType = 'User';
outboxMessage.eventType = EventType.USER_PASSWORD_RESET_REQUESTED;
outboxMessage.eventPayload = JSON.stringify(new AfterUserPasswordResetRequestedEvent(user.id));
user.password = null;
await userRepo.save(user);
await outboxRepo.save(outboxMessage);
});
}
}
And the event handler:
export class AfterUserPasswordResetRequestedListener extends BaseListener<AfterUserPasswordResetRequestedEvent> {
protected queueUrl: string = process.env.AWS_SQS_AFTER_USER_PASSWORD_RESET_REQUESTED_URL;
protected visibilityTimeout: number = 60;
protected workerName: string = 'After User Password Reset Requested Handler';
protected batchSize: number = undefined;
constructor(
private readonly emailService = new EmailService(),
private readonly userService = new UserService(),
private readonly authService = new AuthService()
) {
super();
}
async onMessage(message: AfterUserPasswordResetRequestedEvent): Promise<void> {
try {
const user = await this.userService.findUserById(message.userId);
const loginUrl = await this.authService.createResetPasswordTokenizedUrl(user.email, process.env.FRONTEND_URL);
await this.emailService.send(
new PasswordResetEmail(
{
name: user.name,
loginurl: loginUrl,
},
user.email
)
);
} catch (err) {
APP_LOGGER.error(err);
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(err);
}
throw err;
}
}
}
Is that an overkill? I feel like guilty with both approaches. Using IOC for managing emails increase my SRP problem (small) and unreliable send emails and using EDA looks like more robust and complex but it solves the issue very well. I am also thinking if I am not adding things that may wrongly mixing concepts from both EDA/Layered world.
Any experience with that? It looks like some people did it https://www.toptal.com/software/single-responsibility-principle
I have many services that all need to know the tenant ID from the request (kept in JWT auth token). The request is either GRPC (jwt stored in MetaData) or Graphql (jwt stored in context.headers.authorization).
I would like to be able to force myself not to forget to pass this tenant id when using the services. Ideally I dont want to even have to constantly write the same code to get the info from the request and pass it through. However the only ways I've managed to do it was using:
#Inject(REQUEST) for grpc in the service constructor. This doesn't work for the graphql requests. The only other way I saw was to only return service methods AFTER providing the data, which looks ugly as hell:
class MyService {
private _actions: {
myMethod1() { ... }
}
withTenantDetails(details) {
this._details = details;
return this._actions;
}
}
If I can somehow get the execution context within MyService that would be a good option, and make this easy using:
const getTenantId = (context: ExecutionContext) => {
if (context.getType() === 'rpc') {
logger.debug('received rpc request');
const request = context.switchToRpc().getContext();
const token = request.context.get("x-authorization");
return {
token,
id: parseTokenTenantInfo(token)
};
}
else if (context.getType<GqlContextType>() === 'graphql') {
logger.debug('received graphql request');
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().request;
const token = request.get('Authorization');
return {
token,
id: parseTokenTenantInfo(token)
};
}
else {
throw new Error(`Unknown context type receiving in tenant param decorator`)
}
}
But I can't find any way to get the executioncontext across to the service without also having to remember to pass it every time.
It's possible to inject Request into injectable service.
For that, the Service will be Scope.Request, and no more Singleton, so a new instance will be created for each request. It's an important consideration, to avoid creating too many resources for performance reason.
It's possible to explicit this scope with :
#Injectable({ scope: Scope.REQUEST })
app.service.ts :
#Injectable({ scope: Scope.REQUEST })
export class AppService {
tenantId: string;
constructor(#Inject(REQUEST) private request: Request) {
// because of #Inject(REQUEST),
// this service becomes REQUEST SCOPED
// and no more SINGLETON
// so this will be executed for each request
this.tenantId = getTenantIdFromRequest(this.request);
}
getData(): Data {
// some logic here
return {
tenantId: this.tenantId,
//...
};
}
}
// this is for example...
const getTenantIdFromRequest = (request: Request): string => {
return request?.header('tenant_id');
};
Note that, instead of decode a JWT token in order to retrieve TENANT_ID for each request, and maybe for other service (one per service), an other approach could be to decode JWT one single time, and then add it in Request object.
It could be done with a global Guard, same as authorization guard examples of official docs.
Here just a simple example : (could be merged with a Auth Guard)
#Injectable()
export class TenantIdGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
request['tenantId'] = getTenantIdFromRequest(request);
return true; // or any other validation
}
}
For GraphQL applications, we should inject CONTEXT in place of REQUEST :
constructor(#Inject(CONTEXT) private context) {}
You have to set either request inside context, or directly TENANT_ID inside context in order to retrieve it after inside service.
I have created a simple jwt auth application the same way its displayed here: https://github.com/raymondfeng/loopback4-example-auth0
The authentication part is working properly but the authorization does not work as expected.
I decorated my controller with following function and added a scope.
#authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}})
In my authentication strategy I´m checking the scope via the AuthenticationMetadata class.
import {AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy} from '#loopback/authentication';
import {inject} from '#loopback/core';
import {ExpressRequestHandler, Request, Response, RestBindings} from '#loopback/rest';
import {UserProfile} from '#loopback/security';
import {JWT_SERVICE} from './types';
const jwtAuthz = require('express-jwt-authz');
export class JWTAuthenticationStrategy implements AuthenticationStrategy {
name = 'auth0-jwt';
constructor(
#inject(RestBindings.Http.RESPONSE)
private response: Response,
#inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
#inject(JWT_SERVICE)
private jwtCheck: ExpressRequestHandler,
) {}
async authenticate(request: Request): Promise<UserProfile | undefined> {
return new Promise<UserProfile | undefined>((resolve, reject) => {
this.jwtCheck(request, this.response, (err: unknown) => {
if (err) {
console.error(err);
reject(err);
return;
}
console.log(this.metadata.options);
// If the `#authenticate` requires `scopes` check
if (this.metadata.options?.scopes) {
jwtAuthz(this.metadata.options!.scopes, {failWithError: true})(request, this.response, (err2?: Error) => {
if (err2) {
console.error(err2);
reject(err2);
return;
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
});
} else {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
}
});
});
}
}
When trying to access
this.metadata.options
I´m always getting an undefined back.
How can I achieve to get the options and the scope out of metadata?
Thanks
For Loopback Authorization your class needs to implement the Provider<Authorizer> interface. In that interface it defines the 2 functions you need to implement
#injectable({scope: BindingScope.TRANSIENT})
class AuthorizationService implements Provider<Authorizer>{
value (): Authorizer {
return this.authorize.bind(this);
}
async authorize (
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
// TODO implement authorization
}
}
The authorization metadata will be injected by loopback into that function automatically after you bind it with an AuthorizationTags.Authorizer
If you are having problems implementing Authentication then read my step by step guide on how we implemented Loopback Authentication using Firebase. That should be able to help you with the core ideas to get Authentication running.
Is it possible to have Filters (Auth or Exception) for Azure functions? I just want to not duplicate code to validate bearer token in every function. I see that there is a filter concept in webjobs sdk. https://github.com/Azure/azure-webjobs-sdk/wiki/Function-Filters
I want to only validate the bearer token before executing any function. So if filters are not the best option then is there any other better way to handle this situation ?
Depending on how feature rich you want your responses you could use function filtered but they are very limited at the moment until this issue has been completed - https://github.com/Azure/azure-webjobs-sdk/issues/1314
Alternatively, you could set up a pipeline in each of your functions so you could apply the same cross-cutting concern logic inside your function app. obviously this will be a lot more work but comes with a lot more flexibility.
Example - https://github.com/kevbite/AzureFunctions.GreenPipes
Instead of bringing in another package, you can just pass your function code as an argument in to a wrapper method.
//business logic
[FunctionName("PostWidget")]
public async Task<IActionResult> PostWidget(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "widgets")] Widget item, HttpRequest req, ILogger log)
{
return await _functionWrapper.Execute(req, item, async () =>
{
log.LogInformation($"posting widget: ${item.Id}");
var newItem = await dbContext.Widgets.AddAsync(item);
await dbContext.SaveChangesAsync();
return new ResponseEnvelopeResult<Widget>(HttpStatusCode.Created, newItem.Entity);
});
}
//functionWrapper class
public async Task<IActionResult> Execute(T model, HttpRequest req, Func<Task<IActionResult>> azureFunction)
{
var results = await _validator.ValidateAsync(model, ruleSet: $"default,audit,{req.Method}");
if (!results.IsValid)
{
var errors = results.Errors.Select(x => x.ErrorMessage).ToList();
_log.LogWarning($"Model validation failed for type '{typeof(T).Name}'. Validation errors: [{errors.Join()}] ");
return new ResponseEnvelopeResult<T>(HttpStatusCode.BadRequest, null, errors);
}
try
{
return await azureFunction();
}
catch (Exception e)
{
_log.LogError(e, "Unhandled exception occured in FunctionWrapper");
return new ResponseEnvelopeResult<T>(HttpStatusCode.InternalServerError, null, new[] { e.Message });
}
}
Then your wrapper can be setup to do validation, retrieve user info, etc. If you need items passed back to your function layer, you can easily do so without obscuring your function intent. I've got a large example of this implementation on my blog.
https://blog.bruceleeharrison.com/2019/09/04/azure-v2-functions-with-fluentvalidation/