Initial commit
This commit is contained in:
commit
bb911f8ffb
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
./local-aws.sqlite
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"entryFile": "main.js"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "local-aws",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^9.3.10",
|
||||||
|
"@nestjs/config": "^2.3.1",
|
||||||
|
"@nestjs/core": "^9.3.10",
|
||||||
|
"@nestjs/platform-express": "^9.3.10",
|
||||||
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"joi": "^17.9.0",
|
||||||
|
"js2xmlparser": "^5.0.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"typeorm": "^0.3.12",
|
||||||
|
"uuidv4": "^6.2.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^9.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Action } from './action.enum';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
export type AwsProperties = {
|
||||||
|
accountId: string;
|
||||||
|
region: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Format {
|
||||||
|
Xml,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class AbstractActionHandler<T = Record<string, string | number | boolean>> {
|
||||||
|
|
||||||
|
abstract format: Format;
|
||||||
|
abstract action: Action;
|
||||||
|
abstract validator: Joi.ObjectSchema<T>;
|
||||||
|
protected abstract handle(queryParams: T, awsProperties: AwsProperties): Record<string, any> | void;
|
||||||
|
|
||||||
|
async getResponse(queryParams: T, awsProperties: AwsProperties) {
|
||||||
|
if (this.format === Format.Xml) {
|
||||||
|
return await this.getXmlResponse(queryParams, awsProperties);
|
||||||
|
}
|
||||||
|
return await this.getJsonResponse(queryParams, awsProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getXmlResponse(queryParams: T, awsProperties: AwsProperties) {
|
||||||
|
const response = {
|
||||||
|
'@': {
|
||||||
|
xmlns: "https://sns.amazonaws.com/doc/2010-03-31/"
|
||||||
|
},
|
||||||
|
ResponseMetadata: {
|
||||||
|
RequestId: uuid.v4(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.handle(queryParams, awsProperties);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[`${this.action}Result`]: {
|
||||||
|
...result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getJsonResponse(queryParams: T, awsProperties: AwsProperties) {
|
||||||
|
const result = await this.handle(queryParams, awsProperties);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
export enum Action {
|
||||||
|
|
||||||
|
// SecretsManager
|
||||||
|
SecretsManagerCancelRotateSecret = 'secretsmanager.CancelRotateSecret',
|
||||||
|
SecretsManagerCreateSecret = 'secretsmanager.CreateSecret',
|
||||||
|
SecretsManagerDeleteResourcePolicy = 'secretsmanager.DeleteResourcePolicy',
|
||||||
|
SecretsManagerDeleteSecret = 'secretsmanager.DeleteSecret',
|
||||||
|
SecretsManagerDescribeSecret = 'secretsmanager.DescribeSecret',
|
||||||
|
SecretsManagerGetRandomPassword = 'secretsmanager.GetRandomPassword',
|
||||||
|
SecretsManagerGetResourcePolicy = 'secretsmanager.GetResourcePolicy',
|
||||||
|
SecretsManagerGetSecretValue = 'secretsmanager.GetSecretValue',
|
||||||
|
SecretsManagerListSecrets = 'secretsmanager.ListSecrets',
|
||||||
|
SecretsManagerListSecretVersionIds = 'secretsmanager.ListSecretVersionIds',
|
||||||
|
SecretsManagerPutResourcePolicy = 'secretsmanager.PutResourcePolicy',
|
||||||
|
SecretsManagerPutSecretValue = 'secretsmanager.PutSecretValue',
|
||||||
|
SecretsManagerRemoveRegionsFromReplication = 'secretsmanager.RemoveRegionsFromReplication',
|
||||||
|
SecretsManagerReplicateSecretToRegions = 'secretsmanager.ReplicateSecretToRegions',
|
||||||
|
SecretsManagerRestoreSecret = 'secretsmanager.RestoreSecret',
|
||||||
|
SecretsManagerRotateSecret = 'secretsmanager.RotateSecret',
|
||||||
|
SecretsManagerStopReplicationToReplica = 'secretsmanager.StopReplicationToReplica',
|
||||||
|
SecretsManagerTagResource = 'secretsmanager.TagResource',
|
||||||
|
SecretsManagerUntagResource = 'secretsmanager.UntagResource',
|
||||||
|
SecretsManagerUpdateSecret = 'secretsmanager.UpdateSecret',
|
||||||
|
SecretsManagerUpdateSecretVersionStage = 'secretsmanager.UpdateSecretVersionStage',
|
||||||
|
SecretsManagerValidateResourcePolicy = 'secretsmanager.ValidateResourcePolicy',
|
||||||
|
|
||||||
|
// SNS
|
||||||
|
SnsAddPermission = 'AddPermission',
|
||||||
|
SnsCheckIfPhoneNumberIsOptedOut = 'CheckIfPhoneNumberIsOptedOut',
|
||||||
|
SnsConfirmSubscription = 'ConfirmSubscription',
|
||||||
|
SnsCreatePlatformApplication = 'CreatePlatformApplication',
|
||||||
|
SnsCreatePlatformEndpoint = 'CreatePlatformEndpoint',
|
||||||
|
SnsCreateSMSSandboxPhoneNumber = 'CreateSMSSandboxPhoneNumber',
|
||||||
|
SnsCreateTopic = 'CreateTopic',
|
||||||
|
SnsDeleteEndpoint = 'DeleteEndpoint',
|
||||||
|
SnsDeletePlatformApplication = 'DeletePlatformApplication',
|
||||||
|
SnsDeleteSMSSandboxPhoneNumber = 'DeleteSMSSandboxPhoneNumber',
|
||||||
|
SnsDeleteTopic = 'DeleteTopic',
|
||||||
|
SnsGetDataProtectionPolicy = 'GetDataProtectionPolicy',
|
||||||
|
SnsGetEndpointAttributes = 'GetEndpointAttributes',
|
||||||
|
SnsGetPlatformApplicationAttributes = 'GetPlatformApplicationAttributes',
|
||||||
|
SnsGetSMSAttributes = 'GetSMSAttributes',
|
||||||
|
SnsGetSMSSandboxAccountStatus = 'GetSMSSandboxAccountStatus',
|
||||||
|
SnsGetSubscriptionAttributes = 'GetSubscriptionAttributes',
|
||||||
|
SnsGetTopicAttributes = 'GetTopicAttributes',
|
||||||
|
SnsListEndpointsByPlatformApplication = 'ListEndpointsByPlatformApplication',
|
||||||
|
SnsListOriginationNumbers = 'ListOriginationNumbers',
|
||||||
|
SnsListPhoneNumbersOptedOut = 'ListPhoneNumbersOptedOut',
|
||||||
|
SnsListPlatformApplications = 'ListPlatformApplications',
|
||||||
|
SnsListSMSSandboxPhoneNumbers = 'ListSMSSandboxPhoneNumbers',
|
||||||
|
SnsListSubscriptions = 'ListSubscriptions',
|
||||||
|
SnsListSubscriptionsByTopic = 'ListSubscriptionsByTopic',
|
||||||
|
SnsListTagsForResource = 'ListTagsForResource',
|
||||||
|
SnsListTopics = 'ListTopics',
|
||||||
|
SnsOptInPhoneNumber = 'OptInPhoneNumber',
|
||||||
|
SnsPublish = 'Publish',
|
||||||
|
SnsPublishBatch = 'PublishBatch',
|
||||||
|
SnsPutDataProtectionPolicy = 'PutDataProtectionPolicy',
|
||||||
|
SnsRemovePermission = 'RemovePermission',
|
||||||
|
SnsSetEndpointAttributes = 'SetEndpointAttributes',
|
||||||
|
SnsSetPlatformApplicationAttributes = 'SetPlatformApplicationAttributes',
|
||||||
|
SnsSetSMSAttributes = 'SetSMSAttributes',
|
||||||
|
SnsSetSubscriptionAttributes = 'SetSubscriptionAttributes',
|
||||||
|
SnsSetTopicAttributes = 'SetTopicAttributes',
|
||||||
|
SnsSubscribe = 'Subscribe',
|
||||||
|
SnsTagResource = 'TagResource',
|
||||||
|
SnsUnsubscribe = 'Unsubscribe',
|
||||||
|
SnsUntagResource = 'UntagResource',
|
||||||
|
SnsVerifySMSSandboxPhoneNumber = 'VerifySMSSandboxPhoneNumber',
|
||||||
|
|
||||||
|
// SQS
|
||||||
|
SqsAddPermisson = 'AddPermission',
|
||||||
|
SqsChangeMessageVisibility = 'ChangeMessageVisibility',
|
||||||
|
SqsChangeMessageVisibilityBatch = 'ChangeMessageVisibilityBatch',
|
||||||
|
SqsCreateQueue = 'CreateQueue',
|
||||||
|
SqsDeleteMessage = 'DeleteMessage',
|
||||||
|
SqsDeleteMessageBatch = 'DeleteMessageBatch',
|
||||||
|
SqsDeleteQueue = 'DeleteQueue',
|
||||||
|
SqsGetQueueAttributes = 'GetQueueAttributes',
|
||||||
|
SqsGetQueueUrl = 'GetQueueUrl',
|
||||||
|
SqsListDeadLetterSourceQueues = 'ListDeadLetterSourceQueues',
|
||||||
|
SqsListQueues = 'ListQueues',
|
||||||
|
SqsListQueueTags = 'ListQueueTags',
|
||||||
|
SqsPurgeQueue = 'PurgeQueue',
|
||||||
|
SqsReceiveMessage = 'ReceiveMessage',
|
||||||
|
SqsRemovePermission = 'RemovePermission',
|
||||||
|
SqsSendMessage = 'SendMessage',
|
||||||
|
SqsSendMessageBatch = 'SendMessageBatch',
|
||||||
|
SqsSetQueueAttributes = 'SetQueueAttributes',
|
||||||
|
SqsTagQueue = 'TagQueue',
|
||||||
|
SqsUntagQueue = 'UntagQueue',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AbstractActionHandler } from './abstract-action.handler';
|
||||||
|
import { Action } from './action.enum';
|
||||||
|
|
||||||
|
export type ActionHandlers = Record<Action, AbstractActionHandler>;
|
||||||
|
export const ActionHandlers = Symbol.for('ACTION_HANDLERS');
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { BadRequestException, Body, Controller, Get, Inject, Post, Headers } from '@nestjs/common';
|
||||||
|
import { ActionHandlers } from './app.constants';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { Action } from './action.enum';
|
||||||
|
import { AbstractActionHandler, Format } from './abstract-action.handler';
|
||||||
|
import * as js2xmlparser from 'js2xmlparser';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { CommonConfig } from './config/common-config.interface';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ActionHandlers)
|
||||||
|
private readonly actionHandlers: ActionHandlers,
|
||||||
|
private readonly configService: ConfigService<CommonConfig>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async post(
|
||||||
|
@Body() body: Record<string, any>,
|
||||||
|
@Headers() headers: Record<string, any>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
const lowerCasedHeaders = Object.keys(headers).reduce((o, k) => {
|
||||||
|
o[k.toLocaleLowerCase()] = headers[k];
|
||||||
|
return o;
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const queryParams = { ...body, ...lowerCasedHeaders };
|
||||||
|
console.log({queryParams})
|
||||||
|
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
||||||
|
|
||||||
|
const { error: actionError } = Joi.object({
|
||||||
|
[actionKey]: Joi.string().valid(...Object.values(Action)).required(),
|
||||||
|
}).validate(queryParams, { allowUnknown: true });
|
||||||
|
|
||||||
|
if (actionError) {
|
||||||
|
throw new BadRequestException(actionError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = queryParams[actionKey];
|
||||||
|
const handler: AbstractActionHandler = this.actionHandlers[action];
|
||||||
|
|
||||||
|
const { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
|
||||||
|
|
||||||
|
if (validatorError) {
|
||||||
|
throw new BadRequestException(validatorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION') };
|
||||||
|
|
||||||
|
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
|
||||||
|
if (handler.format === Format.Xml) {
|
||||||
|
const xmlResponse = js2xmlparser.parse(`${handler.action}Response`, jsonResponse);
|
||||||
|
// console.log({xmlResponse})
|
||||||
|
return xmlResponse;
|
||||||
|
}
|
||||||
|
return jsonResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ActionHandlers } from './app.constants';
|
||||||
|
import { CommonConfig } from './config/common-config.interface';
|
||||||
|
import localConfig from './config/local.config';
|
||||||
|
import { SnsHandlers } from './sns/sns.constants';
|
||||||
|
import { SnsModule } from './sns/sns.module';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
|
||||||
|
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
||||||
|
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
load: [localConfig],
|
||||||
|
isGlobal: true,
|
||||||
|
// validationSchema: configValidator,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService<CommonConfig>) => ({
|
||||||
|
type: 'sqlite',
|
||||||
|
database: configService.get('DB_DATABASE'),
|
||||||
|
logging: configService.get('DB_LOGGING'),
|
||||||
|
synchronize: configService.get('DB_SYNCHRONIZE'),
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
SnsModule,
|
||||||
|
SecretsManagerModule,
|
||||||
|
AwsSharedEntitiesModule,
|
||||||
|
],
|
||||||
|
controllers: [
|
||||||
|
AppController,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActionHandlers,
|
||||||
|
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
||||||
|
inject: [
|
||||||
|
SnsHandlers,
|
||||||
|
SecretsManagerHandlers,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('attributes')
|
||||||
|
export class Attribute extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn({ name: 'id' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'arn', nullable: false })
|
||||||
|
@Index()
|
||||||
|
arn: string;
|
||||||
|
|
||||||
|
@Column({ name: 'name', nullable: false })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: 'value', nullable: false })
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Attribute } from './attributes.entity';
|
||||||
|
import { CreateAttributeDto } from './create-attribute.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AttributesService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Attribute)
|
||||||
|
private readonly repo: Repository<Attribute>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getByArn(arn: string): Promise<Attribute[]> {
|
||||||
|
return await this.repo.find({ where: { arn }});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateAttributeDto): Promise<Attribute> {
|
||||||
|
return await this.repo.save(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteByArnAndName(arn: string, name: string) {
|
||||||
|
await this.repo.delete({ arn, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
|
||||||
|
for (const record of records) {
|
||||||
|
await this.create({ arn, name: record.key, value: record.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static attributePairs(queryParams: Record<string, string>): { key: string, value: string }[] {
|
||||||
|
const pairs = [null];
|
||||||
|
for (const param of Object.keys(queryParams)) {
|
||||||
|
const [type, _, idx, slot] = param.split('.');
|
||||||
|
|
||||||
|
if (type === 'Attributes') {
|
||||||
|
if (!pairs[+idx]) {
|
||||||
|
pairs[+idx] = { key: '', value: ''};
|
||||||
|
}
|
||||||
|
pairs[+idx][slot] = queryParams[param];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs.shift();
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getXmlSafeAttributesMap(attributes: Record<string, string>) {
|
||||||
|
return { Attributes: { entry: Object.keys(attributes).map(key => ({ key, value: attributes[key] })) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Attribute } from './attributes.entity';
|
||||||
|
import { AttributesService } from './attributes.service';
|
||||||
|
import { Tag } from './tags.entity';
|
||||||
|
import { TagsService } from './tags.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Attribute, Tag])],
|
||||||
|
providers: [AttributesService, TagsService],
|
||||||
|
exports: [AttributesService, TagsService],
|
||||||
|
})
|
||||||
|
export class AwsSharedEntitiesModule {}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface CreateAttributeDto {
|
||||||
|
arn: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface CreateTagDto {
|
||||||
|
arn: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('tags')
|
||||||
|
export class Tag extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn({ name: 'id' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'arn', nullable: false})
|
||||||
|
@Index()
|
||||||
|
arn: string;
|
||||||
|
|
||||||
|
@Column({ name: 'name', nullable: false })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: 'value', nullable: false })
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Tag } from './tags.entity';
|
||||||
|
import { CreateTagDto } from './create-tag.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagsService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Tag)
|
||||||
|
private readonly repo: Repository<Tag>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getByArn(arn: string): Promise<Tag[]> {
|
||||||
|
return await this.repo.find({ where: { arn }});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateTagDto): Promise<Tag> {
|
||||||
|
return await this.repo.save(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMany(arn: string, records: { Key: string, Value: string }[]): Promise<void> {
|
||||||
|
for (const record of records) {
|
||||||
|
await this.create({ arn, name: record.Key, value: record.Value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteByArnAndName(arn: string, name: string) {
|
||||||
|
await this.repo.delete({ arn, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
static tagPairs(queryParams: Record<string, string>): { Key: string, Value: string }[] {
|
||||||
|
const pairs = [null];
|
||||||
|
for (const param of Object.keys(queryParams)) {
|
||||||
|
const [type, _, idx, slot] = param.split('.');
|
||||||
|
|
||||||
|
if (type === 'Tags') {
|
||||||
|
if (!pairs[+idx]) {
|
||||||
|
pairs[+idx] = { Key: '', Value: ''};
|
||||||
|
}
|
||||||
|
pairs[+idx][slot] = queryParams[param];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs.shift();
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getXmlSafeAttributesMap(tags: Tag[]) {
|
||||||
|
return { Tags: { member: tags.map(({ name, value }) => ({ Key: name, Value: value })) } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface CommonConfig {
|
||||||
|
AWS_ACCOUNT_ID: string;
|
||||||
|
AWS_REGION: string;
|
||||||
|
DB_DATABASE: string;
|
||||||
|
DB_LOGGING?: boolean;
|
||||||
|
DB_SYNCHRONIZE?: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { CommonConfig } from "./common-config.interface";
|
||||||
|
|
||||||
|
export default (): CommonConfig => ({
|
||||||
|
AWS_ACCOUNT_ID: '123456789012',
|
||||||
|
AWS_REGION: 'us-east-1',
|
||||||
|
DB_DATABASE: ':memory:', // 'local-aws.sqlite', // :memory:
|
||||||
|
DB_LOGGING: true,
|
||||||
|
DB_SYNCHRONIZE: true,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
|
||||||
|
export type ExistingActionHandlers = Record<Action, AbstractActionHandler>;
|
||||||
|
export const ExistingActionHandlers = Symbol.for('EXISTING_ACTION_HANDLERS');
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Provider } from '@nestjs/common';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { ExistingActionHandlers } from './default-action-handler.constants';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
||||||
|
|
||||||
|
export const DefaultActionHandlerProvider = (symbol, format: Format, actions: Action[]): Provider => ({
|
||||||
|
provide: symbol,
|
||||||
|
useFactory: (existingActionHandlers: ExistingActionHandlers) => {
|
||||||
|
const cloned = { ...existingActionHandlers };
|
||||||
|
for (const action of actions) {
|
||||||
|
if (!cloned[action]) {
|
||||||
|
cloned[action] = new (class Default extends AbstractActionHandler { action = action; format = format; validator = Joi.object(); handle = () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
},
|
||||||
|
inject: [ExistingActionHandlers]
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Provider } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { ExistingActionHandlers } from './default-action-handler.constants';
|
||||||
|
|
||||||
|
export const ExistingActionHandlersProvider = (inject): Provider => ({
|
||||||
|
provide: ExistingActionHandlers,
|
||||||
|
useFactory: (...args: AbstractActionHandler[]) => args.reduce((m, h) => {
|
||||||
|
m[h.action] = h;
|
||||||
|
return m;
|
||||||
|
}, {}),
|
||||||
|
inject,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum Group {
|
||||||
|
Legacy = 'legacy',
|
||||||
|
SecretsManager = 'secretsmanager',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { ClassSerializerInterceptor } from '@nestjs/common';
|
||||||
|
import { NestFactory, Reflector } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import * as morgan from 'morgan';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
const port = 8081;
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||||
|
|
||||||
|
await app.listen(port, () => console.log(`Listening on port ${port}`));
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('secret')
|
||||||
|
export class Secret extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn({ name: 'id' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
|
||||||
|
export type SecretsManagerHandlers = Record<Action, AbstractActionHandler>;
|
||||||
|
export const SecretsManagerHandlers = Symbol.for('SecretsManagerHandlers');
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
|
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
Action.SecretsManagerCancelRotateSecret,
|
||||||
|
Action.SecretsManagerCreateSecret,
|
||||||
|
Action.SecretsManagerDeleteResourcePolicy,
|
||||||
|
Action.SecretsManagerDeleteSecret,
|
||||||
|
Action.SecretsManagerDescribeSecret,
|
||||||
|
Action.SecretsManagerGetRandomPassword,
|
||||||
|
Action.SecretsManagerGetResourcePolicy,
|
||||||
|
Action.SecretsManagerGetSecretValue,
|
||||||
|
Action.SecretsManagerListSecrets,
|
||||||
|
Action.SecretsManagerListSecretVersionIds,
|
||||||
|
Action.SecretsManagerPutResourcePolicy,
|
||||||
|
Action.SecretsManagerPutSecretValue,
|
||||||
|
Action.SecretsManagerRemoveRegionsFromReplication,
|
||||||
|
Action.SecretsManagerReplicateSecretToRegions,
|
||||||
|
Action.SecretsManagerRestoreSecret,
|
||||||
|
Action.SecretsManagerRotateSecret,
|
||||||
|
Action.SecretsManagerStopReplicationToReplica,
|
||||||
|
Action.SecretsManagerTagResource,
|
||||||
|
Action.SecretsManagerUntagResource,
|
||||||
|
Action.SecretsManagerUpdateSecret,
|
||||||
|
Action.SecretsManagerUpdateSecretVersionStage,
|
||||||
|
Action.SecretsManagerValidateResourcePolicy,
|
||||||
|
]
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([]),
|
||||||
|
AwsSharedEntitiesModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
...handlers,
|
||||||
|
ExistingActionHandlersProvider(handlers),
|
||||||
|
DefaultActionHandlerProvider(SecretsManagerHandlers, Format.Json, actions),
|
||||||
|
],
|
||||||
|
exports: [SecretsManagerHandlers],
|
||||||
|
})
|
||||||
|
export class SecretsManagerModule {}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { SnsTopic } from './sns-topic.entity';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateTopicHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopic)
|
||||||
|
private readonly snsTopicRepo: Repository<SnsTopic>,
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsCreateTopic;
|
||||||
|
validator = Joi.object<QueryParams, true>({ Name: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const { Name: name } = params;
|
||||||
|
|
||||||
|
const topic = await this.snsTopicRepo.create({
|
||||||
|
name,
|
||||||
|
accountId: awsProperties.accountId,
|
||||||
|
region: awsProperties.region,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
const tags = TagsService.tagPairs(params);
|
||||||
|
await this.tagsService.createMany(topic.topicArn, tags);
|
||||||
|
|
||||||
|
return { TopicArn: topic.topicArn };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
SubscriptionArn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopicSubscription)
|
||||||
|
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsGetSubscriptionAttributes;
|
||||||
|
validator = Joi.object<QueryParams, true>({ SubscriptionArn: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle({ SubscriptionArn }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const id = SubscriptionArn.split(':')[-1];
|
||||||
|
const subscription = await this.snsTopicSubscriptionRepo.findOne({ where: { id }});
|
||||||
|
const attributes = await this.attributeService.getByArn(SubscriptionArn);
|
||||||
|
const attributeMap = attributes.reduce((m, a) => {
|
||||||
|
m[a.name] = a.value;
|
||||||
|
return m;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
ConfirmationWasAuthenticated: 'true',
|
||||||
|
PendingConfirmation: 'false',
|
||||||
|
Owner: subscription.accountId,
|
||||||
|
SubscriptionArn: subscription.arn,
|
||||||
|
TopicArn: subscription.topicArn,
|
||||||
|
...attributeMap,
|
||||||
|
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttributesService.getXmlSafeAttributesMap(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { SnsTopic } from './sns-topic.entity';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
TopicArn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetTopicAttributesHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopic)
|
||||||
|
private readonly snsTopicRepo: Repository<SnsTopic>,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsGetTopicAttributes;
|
||||||
|
validator = Joi.object<QueryParams, true>({ TopicArn: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle({ TopicArn }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const name = TopicArn.split(':')[-1];
|
||||||
|
const topic = await this.snsTopicRepo.findOne({ where: { name }});
|
||||||
|
const attributes = await this.attributeService.getByArn(TopicArn);
|
||||||
|
const attributeMap = attributes.reduce((m, a) => {
|
||||||
|
m[a.name] = a.value;
|
||||||
|
return m;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
DisplayName: topic.name,
|
||||||
|
Owner: topic.accountId,
|
||||||
|
SubscriptionsConfirmed: '0',
|
||||||
|
SubscriptionsDeleted: '0',
|
||||||
|
SubscriptionsPending: '0',
|
||||||
|
TopicArn: topic.topicArn,
|
||||||
|
...attributeMap,
|
||||||
|
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttributesService.getXmlSafeAttributesMap(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { SnsTopic } from './sns-topic.entity';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
ResourceArn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListTagsForResourceHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsListTagsForResource;
|
||||||
|
validator = Joi.object<QueryParams, true>({ ResourceArn: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle({ ResourceArn }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
const tags = await this.tagsService.getByArn(ResourceArn);
|
||||||
|
return TagsService.getXmlSafeAttributesMap(tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { SnsTopic } from './sns-topic.entity';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
NextToken: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListTopicsHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopic)
|
||||||
|
private readonly snsTopicRepo: Repository<SnsTopic>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsListTopics;
|
||||||
|
validator = Joi.object<QueryParams, true>({ NextToken: Joi.number().default(0) });
|
||||||
|
|
||||||
|
protected async handle({ NextToken: skip }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const [ topics, total ] = await this.snsTopicRepo.findAndCount({ order: { name: 'DESC' }, take: 100, skip });
|
||||||
|
const response = { Topics: topics.map(t => ({ Topic: { TopicArn: t.topicArn } }))};
|
||||||
|
|
||||||
|
if (total >= 100) {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
NextToken: `${skip + 100}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
AttributeName: string;
|
||||||
|
AttributeValue: string;
|
||||||
|
TopicArn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SetSubscriptionAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsSetSubscriptionAttributes;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
AttributeName: Joi.string().required(),
|
||||||
|
AttributeValue: Joi.string().required(),
|
||||||
|
TopicArn: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
AttributeName: string;
|
||||||
|
AttributeValue: string;
|
||||||
|
TopicArn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SetTopicAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsSetTopicAttributes;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
AttributeName: Joi.string().required(),
|
||||||
|
AttributeValue: Joi.string().required(),
|
||||||
|
TopicArn: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('sns_topic_subscription')
|
||||||
|
export class SnsTopicSubscription extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryColumn({ name: 'id' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'topic_arn' })
|
||||||
|
topicArn: string;
|
||||||
|
|
||||||
|
@Column({ name: 'endpoint', nullable: true })
|
||||||
|
endpoint: string;
|
||||||
|
|
||||||
|
@Column({ name: 'protocol' })
|
||||||
|
protocol: string;
|
||||||
|
|
||||||
|
@Column({ name: 'account_id', nullable: false })
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'region', nullable: false })
|
||||||
|
region: string;
|
||||||
|
|
||||||
|
get arn() {
|
||||||
|
return `${this.topicArn}:${this.id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('sns_topic')
|
||||||
|
export class SnsTopic extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryColumn({ name: 'name' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: 'account_id', nullable: false })
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'region', nullable: false })
|
||||||
|
region: string;
|
||||||
|
|
||||||
|
get topicArn(): string {
|
||||||
|
return `arn:aws:sns:${this.region}:${this.accountId}:${this.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
|
||||||
|
export type SnsHandlers = Record<Action, AbstractActionHandler>;
|
||||||
|
export const SnsHandlers = Symbol.for('SNS_HANDLERS');
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
|
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
|
||||||
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
|
import { CreateTopicHandler } from './create-topic.handler';
|
||||||
|
import { GetSubscriptionAttributesHandler } from './get-subscription-attributes.handler';
|
||||||
|
import { GetTopicAttributesHandler } from './get-topic-attributes.handler';
|
||||||
|
import { ListTagsForResourceHandler } from './list-tags-for-resource.handler';
|
||||||
|
import { ListTopicsHandler } from './list-topics.handler';
|
||||||
|
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
||||||
|
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
||||||
|
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||||
|
import { SnsTopic } from './sns-topic.entity';
|
||||||
|
import { SnsHandlers } from './sns.constants';
|
||||||
|
import { SubscribeHandler } from './subscribe.handler';
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
CreateTopicHandler,
|
||||||
|
GetSubscriptionAttributesHandler,
|
||||||
|
GetTopicAttributesHandler,
|
||||||
|
ListTagsForResourceHandler,
|
||||||
|
ListTopicsHandler,
|
||||||
|
SetSubscriptionAttributesHandler,
|
||||||
|
SetTopicAttributesHandler,
|
||||||
|
SubscribeHandler,
|
||||||
|
];
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
Action.SnsAddPermission,
|
||||||
|
Action.SnsCheckIfPhoneNumberIsOptedOut,
|
||||||
|
Action.SnsConfirmSubscription,
|
||||||
|
Action.SnsCreatePlatformApplication,
|
||||||
|
Action.SnsCreatePlatformEndpoint,
|
||||||
|
Action.SnsCreateSMSSandboxPhoneNumber,
|
||||||
|
Action.SnsCreateTopic,
|
||||||
|
Action.SnsDeleteEndpoint,
|
||||||
|
Action.SnsDeletePlatformApplication,
|
||||||
|
Action.SnsDeleteSMSSandboxPhoneNumber,
|
||||||
|
Action.SnsDeleteTopic,
|
||||||
|
Action.SnsGetDataProtectionPolicy,
|
||||||
|
Action.SnsGetEndpointAttributes,
|
||||||
|
Action.SnsGetPlatformApplicationAttributes,
|
||||||
|
Action.SnsGetSMSAttributes,
|
||||||
|
Action.SnsGetSMSSandboxAccountStatus,
|
||||||
|
Action.SnsGetSubscriptionAttributes,
|
||||||
|
Action.SnsGetTopicAttributes,
|
||||||
|
Action.SnsListEndpointsByPlatformApplication,
|
||||||
|
Action.SnsListOriginationNumbers,
|
||||||
|
Action.SnsListPhoneNumbersOptedOut,
|
||||||
|
Action.SnsListPlatformApplications,
|
||||||
|
Action.SnsListSMSSandboxPhoneNumbers,
|
||||||
|
Action.SnsListSubscriptions,
|
||||||
|
Action.SnsListSubscriptionsByTopic,
|
||||||
|
Action.SnsListTagsForResource,
|
||||||
|
Action.SnsListTopics,
|
||||||
|
Action.SnsOptInPhoneNumber,
|
||||||
|
Action.SnsPublish,
|
||||||
|
Action.SnsPublishBatch,
|
||||||
|
Action.SnsPutDataProtectionPolicy,
|
||||||
|
Action.SnsRemovePermission,
|
||||||
|
Action.SnsSetEndpointAttributes,
|
||||||
|
Action.SnsSetPlatformApplicationAttributes,
|
||||||
|
Action.SnsSetSMSAttributes,
|
||||||
|
Action.SnsSetSubscriptionAttributes,
|
||||||
|
Action.SnsSetTopicAttributes,
|
||||||
|
Action.SnsSubscribe,
|
||||||
|
Action.SnsTagResource,
|
||||||
|
Action.SnsUnsubscribe,
|
||||||
|
Action.SnsUntagResource,
|
||||||
|
Action.SnsVerifySMSSandboxPhoneNumber,
|
||||||
|
]
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
|
||||||
|
AwsSharedEntitiesModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
...handlers,
|
||||||
|
ExistingActionHandlersProvider(handlers),
|
||||||
|
DefaultActionHandlerProvider(SnsHandlers, Format.Xml, actions),
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SnsHandlers,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SnsModule {}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||||
|
import { uuid } from 'uuidv4';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
TopicArn: string;
|
||||||
|
Protocol: string;
|
||||||
|
Endpoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopicSubscription)
|
||||||
|
private readonly snsTopicSubscription: Repository<SnsTopicSubscription>,
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsSubscribe;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
Endpoint: Joi.string(),
|
||||||
|
TopicArn: Joi.string().required(),
|
||||||
|
Protocol: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const subscription = await this.snsTopicSubscription.create({
|
||||||
|
id: uuid(),
|
||||||
|
topicArn: params.TopicArn,
|
||||||
|
protocol: params.Protocol,
|
||||||
|
endpoint: params.Endpoint,
|
||||||
|
accountId: awsProperties.accountId,
|
||||||
|
region: awsProperties.region,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
const tags = TagsService.tagPairs(params);
|
||||||
|
await this.tagsService.createMany(subscription.arn, tags);
|
||||||
|
|
||||||
|
const attributes = AttributesService.attributePairs(params);
|
||||||
|
await this.attributeService.createMany(subscription.arn, attributes);
|
||||||
|
|
||||||
|
return { SubscriptionArn: subscription.arn };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue