@vendure/core
Version:
A modern, headless ecommerce framework
311 lines • 15.3 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiKeyService = void 0;
const common_1 = require("@nestjs/common");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const typeorm_1 = require("typeorm");
const common_2 = require("../../common");
const config_1 = require("../../config");
const connection_1 = require("../../connection");
const entity_1 = require("../../entity");
const api_key_translation_entity_1 = require("../../entity/api-key/api-key-translation.entity");
const api_key_entity_1 = require("../../entity/api-key/api-key.entity");
const event_bus_1 = require("../../event-bus");
const api_key_event_1 = require("../../event-bus/events/api-key-event");
const custom_field_relation_service_1 = require("../helpers/custom-field-relation/custom-field-relation.service");
const list_query_builder_1 = require("../helpers/list-query-builder/list-query-builder");
const translatable_saver_1 = require("../helpers/translatable-saver/translatable-saver");
const translator_service_1 = require("../helpers/translator/translator.service");
const get_user_channels_permissions_1 = require("../helpers/utils/get-user-channels-permissions");
const channel_service_1 = require("./channel.service");
const role_service_1 = require("./role.service");
const session_service_1 = require("./session.service");
const user_service_1 = require("./user.service");
let ApiKeyService = class ApiKeyService {
constructor(channelService, configService, connection, customFieldRelationService, eventBus, listQueryBuilder, roleService, sessionService, translatableSaver, translator, userService) {
this.channelService = channelService;
this.configService = configService;
this.connection = connection;
this.customFieldRelationService = customFieldRelationService;
this.eventBus = eventBus;
this.listQueryBuilder = listQueryBuilder;
this.roleService = roleService;
this.sessionService = sessionService;
this.translatableSaver = translatableSaver;
this.translator = translator;
this.userService = userService;
}
/**
* @description
* Returns the appropriate {@link ApiKeyStrategy} based on the {@link ApiType}.
* This is needed because the admin and shop ApiKeyStrategy may differ.
*/
getApiKeyStrategyByApiType(apiType) {
return apiType === 'admin'
? this.configService.authOptions.adminApiKeyStrategy
: this.configService.authOptions.shopApiKeyStrategy;
}
/**
* @description
* Checks that the active user is allowed to grant the specified Roles for an API-Key
*
* // TODO this is taken & slightly modified from adminservice, could merge to not repeat logic
*
* @throws {UserInputError} If the active User has insufficient permissions
* @returns Role-Entities with relations to Channels
*/
async assertActiveUserCanGrantRoles(ctx, roleIds) {
if (roleIds.length === 0)
return [];
const roles = await this.connection.getRepository(ctx, entity_1.Role).find({
where: { id: (0, typeorm_1.In)(roleIds) },
relations: { channels: true },
});
const permissionsRequired = (0, get_user_channels_permissions_1.getChannelPermissions)(roles);
for (const channelPermissions of permissionsRequired) {
const isAllowed = await this.roleService.userHasAllPermissionsOnChannel(ctx, channelPermissions.id, channelPermissions.permissions);
if (!isAllowed)
throw new common_2.UserInputError('error.active-user-does-not-have-sufficient-permissions');
}
return roles;
}
/**
* @description
* Simple user identifier generation function because it is a non-nullable field on User.
*
* Because this simply appends the lookupId, some databases like MySQL/Maria may run into
* length issues if the lookupId has too many characters. Practically speaking this
* should not happen but worth to keep in mind.
*
* @internal
*/
generateApiKeyUserIdentifier(lookupId) {
return `apikey-user-${lookupId}`;
}
/**
* @description
* Creates a new API-Key for the given User
*
* **Important**: The caller is responsible for avoiding privilege escalations by
* verifying `userIdOwner` and `userIdApiKeyUser`; **Use this with great care!**
*
* If you allow users to specify these IDs, they may leak existing User IDs via thrown errors.
*
* @throws {EntityNotFoundError} When either Owner or ApiKeyUser cannot be found
* @throws {UserInputError} When the User tries to grant a role which they themselves dont have
*/
async create(ctx, input, userIdOwner,
/**
* Optionally allow overriding the creation of a separate User.
* This is an advanced use case for plugin-authors to allow impersonation.
* You are responsible for avoiding privilege escalation by verifying this ID.
*/
userIdApiKeyUser) {
const roles = await this.assertActiveUserCanGrantRoles(ctx, input.roleIds);
const ownerUser = await this.connection.getEntityOrThrow(ctx, entity_1.User, userIdOwner);
const strategy = this.getApiKeyStrategyByApiType(ctx.apiType);
const lookupId = await strategy.generateLookupId(ctx);
const apiKeyUser = userIdApiKeyUser
? await this.connection.getEntityOrThrow(ctx, entity_1.User, userIdApiKeyUser, {
// ApiKeyUsers generally require roles and their channels, its important for sessions!
relations: { roles: { channels: true } },
})
: await this.userService.createApiKeyUser(ctx, roles, this.generateApiKeyUserIdentifier(lookupId));
const secret = await strategy.generateSecret(ctx);
const apiKey = strategy.constructApiKey(lookupId, secret);
const hash = await strategy.hashingStrategy.hash(apiKey);
const newEntity = await this.translatableSaver.create({
ctx,
input,
entityType: api_key_entity_1.ApiKey,
translationType: api_key_translation_entity_1.ApiKeyTranslation,
beforeSave: async (e) => {
e.ownerId = ownerUser.id;
e.userId = apiKeyUser.id;
e.apiKeyHash = hash;
e.lookupId = lookupId;
await this.channelService.assignToCurrentChannel(e, ctx);
},
});
await this.customFieldRelationService.updateRelations(ctx, api_key_entity_1.ApiKey, input, newEntity);
// Important: The hash becomes the session token, this is what allows us to authorize on a per-request basis
// Important: The User of the session may be new User to allow configuring separate permissions
await this.sessionService.createNewAuthenticatedSession(ctx, apiKeyUser, config_1.API_KEY_AUTH_STRATEGY_NAME, hash);
config_1.Logger.verbose(`Created ApiKey (${newEntity.id}) for User (${userIdOwner}) with ApiKeyUser (${apiKeyUser.id}, ${apiKeyUser.identifier})`);
await this.eventBus.publish(new api_key_event_1.ApiKeyEvent(ctx, newEntity, 'created', input));
return { apiKey, entityId: newEntity.id };
}
/**
* @description
* Updates an API-Key. Is Channel-Aware.
*
* @throws {EntityNotFoundError} If API-Key cannot be found
*/
async update(ctx, input, relations) {
const entity = await this.connection.getEntityOrThrow(ctx, api_key_entity_1.ApiKey, input.id, {
channelId: ctx.channelId,
relations: ['user'],
});
if (input.roleIds) {
entity.user.roles = await this.assertActiveUserCanGrantRoles(ctx, input.roleIds);
}
const apiKey = await this.translatableSaver.update({
ctx,
input,
entityType: api_key_entity_1.ApiKey,
translationType: api_key_translation_entity_1.ApiKeyTranslation,
beforeSave: async () => {
// Keep in mind that if the user of the ApiKey is being impersonated,
// this would change the roles of the impersonated user!
if (input.roleIds) {
await this.connection.getRepository(ctx, entity_1.User).save(entity.user, { reload: false });
}
},
});
await this.customFieldRelationService.updateRelations(ctx, api_key_entity_1.ApiKey, input, apiKey);
config_1.Logger.verbose(`Updated ApiKey (${apiKey.id}) by User (${String(ctx.activeUserId)})`);
await this.eventBus.publish(new api_key_event_1.ApiKeyEvent(ctx, apiKey, 'updated', input));
return (0, common_2.assertFound)(this.findOne(ctx, input.id, relations));
}
/**
* @description
* Soft-Deletes an API-Key and removes its session. Is Channel-Aware.
*
* @throws {EntityNotFoundError} If API-Key cannot be found
*/
async softDelete(ctx, id) {
const apiKey = await this.connection.getEntityOrThrow(ctx, api_key_entity_1.ApiKey, id, {
channelId: ctx.channelId,
});
const hasAuthMethod = await this.connection.getRepository(ctx, entity_1.AuthenticationMethod).existsBy({
user: { id: apiKey.userId },
});
// If this is an impersonated user who can login, we dont want to delete them
if (hasAuthMethod) {
await this.sessionService.deleteApiKeySession(ctx, apiKey);
}
// If this is an underlying user solely for holding permission, delete them
else {
// SoftDelete should also delete the related sessions & cache
await this.userService.softDelete(ctx, apiKey.userId);
}
apiKey.deletedAt = new Date();
await this.connection
.getRepository(ctx, api_key_entity_1.ApiKey)
.update({ id: apiKey.id }, { deletedAt: apiKey.deletedAt });
config_1.Logger.verbose(`Deleted ApiKey (${id}) by User (${String(ctx.activeUserId)})`);
await this.eventBus.publish(new api_key_event_1.ApiKeyEvent(ctx, apiKey, 'deleted', id));
return { result: generated_types_1.DeletionResult.DELETED };
}
/**
* @description
* Replaces the old with a new API-Key.
*
* This is a convenience method to invalidate an API-Key without
* deleting the underlying roles and permissions.
*
* @throws {EntityNotFoundError} If API-Key cannot be found
*/
async rotate(ctx, id) {
const entity = await this.connection.getEntityOrThrow(ctx, api_key_entity_1.ApiKey, id, {
channelId: ctx.channelId,
includeSoftDeleted: false,
// Need roles and channels for session
relations: { user: { roles: { channels: true } } },
});
const strategy = this.getApiKeyStrategyByApiType(ctx.apiType);
const secret = await strategy.generateSecret(ctx);
const apiKey = strategy.constructApiKey(entity.lookupId, secret);
const hash = await strategy.hashingStrategy.hash(apiKey);
await this.sessionService.deleteApiKeySession(ctx, entity);
await this.sessionService.createNewAuthenticatedSession(ctx, entity.user, config_1.API_KEY_AUTH_STRATEGY_NAME, hash);
entity.apiKeyHash = hash;
await this.connection.getRepository(ctx, api_key_entity_1.ApiKey).save(entity, { reload: false });
config_1.Logger.verbose(`Rotated ApiKey (${entity.id}) by User (${String(ctx.activeUserId)})`);
await this.eventBus.publish(new api_key_event_1.ApiKeyEvent(ctx, entity, 'updated', id));
return { apiKey };
}
/**
* @description
* Is channel-/ and soft-delete aware, translates the entity as well.
*/
async findOne(ctx, id, relations) {
const entity = await this.connection.findOneInChannel(ctx, api_key_entity_1.ApiKey, id, ctx.channelId, {
relations,
where: { deletedAt: (0, typeorm_1.IsNull)() },
});
if (!entity)
return null;
return this.translator.translate(entity, ctx);
}
/**
* @description
* Is channel-/ and soft-delete aware, translates the entity as well.
*/
async findAll(ctx, options, relations) {
return this.listQueryBuilder
.build(api_key_entity_1.ApiKey, options, {
ctx,
relations,
channelId: ctx.channelId,
where: { deletedAt: (0, typeorm_1.IsNull)() },
})
.getManyAndCount()
.then(([notifications, totalItems]) => {
const items = notifications.map(n => this.translator.translate(n, ctx));
return { items, totalItems };
});
}
/**
* @description
* Is channel-/ and soft-delete aware, translates the entity as well.
*/
async findOneByLookupId(ctx, lookupId, relations) {
const entity = await this.connection.getRepository(ctx, api_key_entity_1.ApiKey).findOne({
relations: [...(relations !== null && relations !== void 0 ? relations : []), 'channels'],
where: {
lookupId,
deletedAt: (0, typeorm_1.IsNull)(),
channels: { id: ctx.channelId },
},
});
if (!entity)
return null;
return this.translator.translate(entity, ctx);
}
/**
* @description
* Helper, intended for the AuthGuard to quickly update the lastUsedAt timestamp
*/
async updateLastUsedAtByLookupId(lookupId) {
return this.connection.rawConnection
.getRepository(api_key_entity_1.ApiKey)
.update({ lookupId }, { lastUsedAt: new Date() });
}
};
exports.ApiKeyService = ApiKeyService;
exports.ApiKeyService = ApiKeyService = __decorate([
(0, common_1.Injectable)(),
(0, common_2.Instrument)(),
__metadata("design:paramtypes", [channel_service_1.ChannelService,
config_1.ConfigService,
connection_1.TransactionalConnection,
custom_field_relation_service_1.CustomFieldRelationService,
event_bus_1.EventBus,
list_query_builder_1.ListQueryBuilder,
role_service_1.RoleService,
session_service_1.SessionService,
translatable_saver_1.TranslatableSaver,
translator_service_1.TranslatorService,
user_service_1.UserService])
], ApiKeyService);
//# sourceMappingURL=api-key.service.js.map