Expand kv api
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/multer": "^1.4.10",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/slate-irc": "^0.0.29",
|
"@types/slate-irc": "^0.0.29",
|
||||||
|
@@ -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,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
HttpException,
|
HttpException,
|
||||||
|
Patch,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { KvService } from './kv.service';
|
import { KvService } from './kv.service';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import exp from 'constants';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
|
||||||
@Controller('kv')
|
@Controller('kv')
|
||||||
|
@ApiTags('kv')
|
||||||
export class KvController {
|
export class KvController {
|
||||||
constructor(private readonly kvService: KvService) {}
|
constructor(private readonly kvService: KvService) {}
|
||||||
|
|
||||||
@@ -42,28 +49,32 @@ export class KvController {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (request.headers['x-secret-key']) {
|
if (request.headers['x-secret-key']) {
|
||||||
try {
|
try {
|
||||||
return await this.kvService.getWithSecretKey(
|
return (
|
||||||
|
await this.kvService.getWithSecretKey(
|
||||||
namespace,
|
namespace,
|
||||||
key,
|
key,
|
||||||
request.headers['x-secret-key'] as string,
|
request.headers['x-secret-key'] as string,
|
||||||
);
|
)
|
||||||
|
).toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new HttpException(e.message, 403);
|
throw new HttpException(e.message, 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await this.kvService.get(namespace, key);
|
return (await this.kvService.get(namespace, key)).toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new HttpException(e.message, 403);
|
throw new HttpException(e.message, 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':namespace/:key')
|
@Post(':namespace/:key')
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async create(
|
async create(
|
||||||
@Param('namespace') namespace: string,
|
@Param('namespace') namespace: string,
|
||||||
@Param('key') key: string,
|
@Param('key') key: string,
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Body() body: object,
|
@Body() body: object | string,
|
||||||
|
@UploadedFile('file') file: Express.Multer.File,
|
||||||
) {
|
) {
|
||||||
const secretKey =
|
const secretKey =
|
||||||
(request.headers['x-secret-key'] as string | undefined) ?? '';
|
(request.headers['x-secret-key'] as string | undefined) ?? '';
|
||||||
@@ -72,14 +83,42 @@ export class KvController {
|
|||||||
return await this.kvService.set(
|
return await this.kvService.set(
|
||||||
namespace,
|
namespace,
|
||||||
key,
|
key,
|
||||||
JSON.stringify(body),
|
file === undefined
|
||||||
|
? typeof body === 'object'
|
||||||
|
? Buffer.from(JSON.stringify(body))
|
||||||
|
: Buffer.from(body)
|
||||||
|
: file.buffer,
|
||||||
secretKey,
|
secretKey,
|
||||||
{ public: publicFlag },
|
{ public: publicFlag, mimeType: file.mimetype ?? 'text/plain' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return 'No secret key provided within X-Secret-Key header';
|
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')
|
@Post(':namespace')
|
||||||
async claimNamespace(
|
async claimNamespace(
|
||||||
@Param('namespace') namespace: string,
|
@Param('namespace') namespace: string,
|
||||||
@@ -91,4 +130,22 @@ export class KvController {
|
|||||||
throw new HttpException(e.message, 403);
|
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(
|
public async queryObjectMetadata(
|
||||||
namespace: string,
|
namespace: string,
|
||||||
key: string,
|
key: string,
|
||||||
@@ -48,34 +67,35 @@ export class KvService {
|
|||||||
return objectMetadata?.metaData;
|
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(
|
const objectMetadata = await this.minioService.getObjectMetadata(
|
||||||
this.kvBucketName,
|
this.kvBucketName,
|
||||||
this.generateFilePath(namespace, key),
|
this.generateFilePath(namespace, key),
|
||||||
);
|
);
|
||||||
console.log(objectMetadata);
|
|
||||||
if (objectMetadata?.metaData.public !== 'true') {
|
if (objectMetadata?.metaData.public !== 'true') {
|
||||||
throw new Error('Object is not public');
|
throw new Error('Object is not public');
|
||||||
}
|
}
|
||||||
return await this.minioService
|
return await this.minioService.getBuffer(
|
||||||
.getBuffer(this.kvBucketName, this.generateFilePath(namespace, key))
|
this.kvBucketName,
|
||||||
.then((buffer) => buffer.toString());
|
this.generateFilePath(namespace, key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getWithSecretKey(
|
public async getWithSecretKey(
|
||||||
namespace: string,
|
namespace: string,
|
||||||
key: string,
|
key: string,
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
): Promise<string> {
|
): Promise<Buffer> {
|
||||||
const metadata = await this.getMetadataFile().then((buffer) =>
|
const metadata = await this.getMetadataFile().then((buffer) =>
|
||||||
JSON.parse(buffer.toString()),
|
JSON.parse(buffer.toString()),
|
||||||
);
|
);
|
||||||
if (metadata[namespace]?.secretKey !== secretKey) {
|
if (metadata[namespace]?.secretKey !== secretKey) {
|
||||||
throw new Error('Secret key does not match');
|
throw new Error('Secret key does not match');
|
||||||
}
|
}
|
||||||
return await this.minioService
|
return await this.minioService.getBuffer(
|
||||||
.getBuffer(this.kvBucketName, this.generateFilePath(namespace, key))
|
this.kvBucketName,
|
||||||
.then((buffer) => buffer.toString());
|
this.generateFilePath(namespace, key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async claimNamespace(
|
public async claimNamespace(
|
||||||
@@ -90,7 +110,7 @@ export class KvService {
|
|||||||
}
|
}
|
||||||
metadata[namespace] = {
|
metadata[namespace] = {
|
||||||
...extraData,
|
...extraData,
|
||||||
secretKey: Math.random().toString(36).substring(2),
|
secretKey: this.generateSecretKey(),
|
||||||
claimed: new Date().toISOString(),
|
claimed: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
await this.setMetadataFile(Buffer.from(JSON.stringify(metadata)));
|
await this.setMetadataFile(Buffer.from(JSON.stringify(metadata)));
|
||||||
@@ -100,10 +120,11 @@ export class KvService {
|
|||||||
public async set(
|
public async set(
|
||||||
namespace: string,
|
namespace: string,
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: Buffer,
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
options: {
|
options: {
|
||||||
public: boolean;
|
public: boolean;
|
||||||
|
mimeType?: string;
|
||||||
} = {
|
} = {
|
||||||
public: false,
|
public: false,
|
||||||
},
|
},
|
||||||
@@ -132,4 +153,24 @@ export class KvService {
|
|||||||
this.generateFilePath(namespace, key),
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,9 @@ async function bootstrap() {
|
|||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('api', app, document);
|
SwaggerModule.setup('api', app, document);
|
||||||
app.enableCors({origin: "*"});
|
app.enableCors({ origin: '*' });
|
||||||
|
app.useBodyParser('text');
|
||||||
|
app.disable('x-powered-by');
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { MinioService } from './minio.service';
|
|
||||||
|
|
||||||
describe('MinioService', () => {
|
|
||||||
let service: MinioService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [MinioService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<MinioService>(MinioService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -88,4 +88,19 @@ export class MinioService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.client.removeObject(bucketName, objectName);
|
await this.client.removeObject(bucketName, objectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param bucketName
|
||||||
|
* @param objectName
|
||||||
|
* @param expiry Expiry in seconds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async generatePresignedUrl(
|
||||||
|
bucketName: string,
|
||||||
|
objectName: string,
|
||||||
|
expiry: number = 60 * 60 * 24,
|
||||||
|
): Promise<string> {
|
||||||
|
return await this.client.presignedGetObject(bucketName, objectName, expiry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
yarn.lock
17
yarn.lock
@@ -1003,6 +1003,16 @@
|
|||||||
"@types/range-parser" "*"
|
"@types/range-parser" "*"
|
||||||
"@types/send" "*"
|
"@types/send" "*"
|
||||||
|
|
||||||
|
"@types/express@*":
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||||
|
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/body-parser" "*"
|
||||||
|
"@types/express-serve-static-core" "^4.17.33"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/serve-static" "*"
|
||||||
|
|
||||||
"@types/express@^4.17.17":
|
"@types/express@^4.17.17":
|
||||||
version "4.17.17"
|
version "4.17.17"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
|
||||||
@@ -1074,6 +1084,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||||
|
|
||||||
|
"@types/multer@^1.4.10":
|
||||||
|
version "1.4.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.10.tgz#6bca159aaaf40ec130e99831a08e3d0ed54be611"
|
||||||
|
integrity sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20.3.1":
|
"@types/node@*", "@types/node@^20.3.1":
|
||||||
version "20.5.9"
|
version "20.5.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a"
|
||||||
|
Reference in New Issue
Block a user