Expand kv api
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user