Expand kv api

This commit is contained in:
2023-11-19 22:35:56 -07:00
parent 7c40650b9a
commit 5a54a9ca5b
9 changed files with 154 additions and 75 deletions

View File

@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { KvController } from './KvController';
describe('KvController', () => {
let controller: KvController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [KvController],
}).compile();
controller = module.get<KvController>(KvController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -6,11 +6,18 @@ import {
Body,
Param,
HttpException,
Patch,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { KvService } from './kv.service';
import { Request } from 'express';
import { ApiTags } from '@nestjs/swagger';
import exp from 'constants';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('kv')
@ApiTags('kv')
export class KvController {
constructor(private readonly kvService: KvService) {}
@@ -42,28 +49,32 @@ export class KvController {
): Promise<string> {
if (request.headers['x-secret-key']) {
try {
return await this.kvService.getWithSecretKey(
namespace,
key,
request.headers['x-secret-key'] as string,
);
return (
await this.kvService.getWithSecretKey(
namespace,
key,
request.headers['x-secret-key'] as string,
)
).toString();
} catch (e) {
throw new HttpException(e.message, 403);
}
}
try {
return await this.kvService.get(namespace, key);
return (await this.kvService.get(namespace, key)).toString();
} catch (e) {
throw new HttpException(e.message, 403);
}
}
@Post(':namespace/:key')
@UseInterceptors(FileInterceptor('file'))
async create(
@Param('namespace') namespace: string,
@Param('key') key: string,
@Req() request: Request,
@Body() body: object,
@Body() body: object | string,
@UploadedFile('file') file: Express.Multer.File,
) {
const secretKey =
(request.headers['x-secret-key'] as string | undefined) ?? '';
@@ -72,14 +83,42 @@ export class KvController {
return await this.kvService.set(
namespace,
key,
JSON.stringify(body),
file === undefined
? typeof body === 'object'
? Buffer.from(JSON.stringify(body))
: Buffer.from(body)
: file.buffer,
secretKey,
{ public: publicFlag },
{ public: publicFlag, mimeType: file.mimetype ?? 'text/plain' },
);
}
return 'No secret key provided within X-Secret-Key header';
}
@Post(':namespace/:key/share')
async share(
@Param('namespace') namespace: string,
@Param('key') key: string,
@Req() request: Request,
@Body() body: { expiry?: number },
) {
const secretKey =
(request.headers['x-secret-key'] as string | undefined) ?? '';
if (secretKey) {
return {
expiry: body.expiry,
namespace,
key,
url: await this.kvService.shareFile(
namespace,
key,
secretKey,
body.expiry,
),
};
}
}
@Post(':namespace')
async claimNamespace(
@Param('namespace') namespace: string,
@@ -91,4 +130,22 @@ export class KvController {
throw new HttpException(e.message, 403);
}
}
@Patch(':namespace/secretKey')
async rotateSecretKey(
@Param('namespace') namespace: string,
@Req() request: Request,
) {
if (!request.headers['x-secret-key']) {
throw new HttpException("Missing 'X-Secret-Key' header", 403);
}
try {
return await this.kvService.rotateSecretKey(
namespace,
request.headers['x-secret-key'] as string,
);
} catch (e) {
throw new HttpException(e.message, 403);
}
}
}

View File

@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { KvService } from './kv.service';
describe('KvService', () => {
let service: KvService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [KvService],
}).compile();
service = module.get<KvService>(KvService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -30,6 +30,25 @@ export class KvService {
);
}
private generateSecretKey(): string {
return Math.random().toString(36).substring(2);
}
public async rotateSecretKey(
namespace: string,
secretKey: string,
): Promise<object> {
const metadata = await this.getMetadataFile().then((buffer) =>
JSON.parse(buffer.toString()),
);
if (metadata[namespace]?.secretKey !== secretKey) {
throw new Error('Secret key does not match');
}
metadata[namespace].secretKey = this.generateSecretKey();
await this.setMetadataFile(Buffer.from(JSON.stringify(metadata)));
return metadata[namespace];
}
public async queryObjectMetadata(
namespace: string,
key: string,
@@ -48,34 +67,35 @@ export class KvService {
return objectMetadata?.metaData;
}
public async get(namespace: string, key: string): Promise<string> {
public async get(namespace: string, key: string): Promise<Buffer> {
const objectMetadata = await this.minioService.getObjectMetadata(
this.kvBucketName,
this.generateFilePath(namespace, key),
);
console.log(objectMetadata);
if (objectMetadata?.metaData.public !== 'true') {
throw new Error('Object is not public');
}
return await this.minioService
.getBuffer(this.kvBucketName, this.generateFilePath(namespace, key))
.then((buffer) => buffer.toString());
return await this.minioService.getBuffer(
this.kvBucketName,
this.generateFilePath(namespace, key),
);
}
public async getWithSecretKey(
namespace: string,
key: string,
secretKey: string,
): Promise<string> {
): Promise<Buffer> {
const metadata = await this.getMetadataFile().then((buffer) =>
JSON.parse(buffer.toString()),
);
if (metadata[namespace]?.secretKey !== secretKey) {
throw new Error('Secret key does not match');
}
return await this.minioService
.getBuffer(this.kvBucketName, this.generateFilePath(namespace, key))
.then((buffer) => buffer.toString());
return await this.minioService.getBuffer(
this.kvBucketName,
this.generateFilePath(namespace, key),
);
}
public async claimNamespace(
@@ -90,7 +110,7 @@ export class KvService {
}
metadata[namespace] = {
...extraData,
secretKey: Math.random().toString(36).substring(2),
secretKey: this.generateSecretKey(),
claimed: new Date().toISOString(),
};
await this.setMetadataFile(Buffer.from(JSON.stringify(metadata)));
@@ -100,10 +120,11 @@ export class KvService {
public async set(
namespace: string,
key: string,
value: string,
value: Buffer,
secretKey: string,
options: {
public: boolean;
mimeType?: string;
} = {
public: false,
},
@@ -132,4 +153,24 @@ export class KvService {
this.generateFilePath(namespace, key),
);
}
public async shareFile(
namespace: string,
key: string,
secretKey: string,
// Default expiry is 7 days
expiry: number = 60 * 60 * 24 * 7,
): Promise<string> {
const metadata = await this.getMetadataFile().then((buffer) =>
JSON.parse(buffer.toString()),
);
if (metadata[namespace]?.secretKey !== secretKey) {
throw new Error('Incorrect secret key for namespace');
}
return await this.minioService.generatePresignedUrl(
this.kvBucketName,
this.generateFilePath(namespace, key),
expiry,
);
}
}