Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
andreacw5 committed May 1, 2024
2 parents cc012dc + f80105f commit dd4c939
Show file tree
Hide file tree
Showing 22 changed files with 707 additions and 19 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fileharbor",
"version": "1.0.1",
"version": "1.1.0",
"description": "FileHarbor is a file storage service that allows you to upload, download, and delete files",
"author": "Andrea Tombolato <andreacw96@gmail.com> (https://github.com/andreacw5)",
"private": true,
Expand Down Expand Up @@ -32,9 +32,11 @@
"@prisma/client": "^5.13.0",
"@types/multer": "^1.4.11",
"axios": "^1.6.8",
"bcrypt": "^5.1.1",
"cache-manager": "^5.5.1",
"class-transformer": "^0.5.1",
"file-type-mime": "^0.3.10",
"generate-password-ts": "^1.6.5",
"joi": "^17.13.0",
"passport": "^0.7.0",
"passport-headerapikey": "^1.2.2",
Expand All @@ -50,6 +52,7 @@
"@types/cache-manager": "^4.0.6",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/bcrypt": "^5.0.2",
"@types/multer": "^1.4.11",
"@types/node": "^20.11.26",
"@types/supertest": "^6.0.2",
Expand Down
2 changes: 2 additions & 0 deletions prisma/migrations/20240430220434_optimization/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "localFiles" ADD COLUMN "optimized" BOOLEAN NOT NULL DEFAULT false;
29 changes: 29 additions & 0 deletions prisma/migrations/20240501193248_owners/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Warnings:
- Added the required column `ownerId` to the `localFiles` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "localFiles" ADD COLUMN "ownerId" TEXT NOT NULL,
ADD COLUMN "size" INTEGER;

-- CreateTable
CREATE TABLE "owners" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"externalId" TEXT NOT NULL,
"domain" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "owners_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "owners_externalId_domain_key" ON "owners"("externalId", "domain");

-- AddForeignKey
ALTER TABLE "localFiles" ADD CONSTRAINT "localFiles_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "owners"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
18 changes: 18 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,30 @@ model LocalFile {
filename String
path String
mimetype String
size Int?
type String @default("local")
tags String[]
views Int @default(0)
downloads Int @default(0)
description String?
optimized Boolean @default(false)
owner Owner? @relation(fields: [ownerId], references: [id])
ownerId String
@@map(name: "localFiles")
}

model Owner {
id String @id @default(uuid())
name String?
email String?
externalId String
domain String
password String @default(uuid())
files LocalFile[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([externalId, domain])
@@map(name: "owners")
}
4 changes: 3 additions & 1 deletion src/modules/v1/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { HeaderApiKeyStrategy } from './key.strategy';
import { ConfigModule } from '@nestjs/config';
import { AuthService } from './auth.service';

@Module({
imports: [PassportModule, ConfigModule],
providers: [HeaderApiKeyStrategy],
providers: [HeaderApiKeyStrategy, AuthService],
exports: [AuthService],
})
export class AuthModule {}
29 changes: 29 additions & 0 deletions src/modules/v1/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import generator from 'generate-password-ts';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
/**
* Generates a password
*/
async generateAPassword() {
const password = generator.generate({
length: 30,
numbers: true,
symbols: true,
});

const saltOrRounds = 15;
return await bcrypt.hash(password, saltOrRounds);
}

/**
* Compares a password with a hash
* @param password
* @param hash
*/
async comparePassword(password: string, hash: string) {
return await bcrypt.compare(password, hash);
}
}
2 changes: 2 additions & 0 deletions src/modules/v1/localFiles/dto/create-local-file.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export class CreateLocalFileDto {
externalId: string;
domain: string;
description?: string;
tags?: string[];
type?: string;
Expand Down
25 changes: 25 additions & 0 deletions src/modules/v1/localFiles/dto/local-file-filter.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';

export class LocalFileFilterDto {
@ApiProperty({
description: 'Type of file',
enum: ['local', 'avatar'],
required: false,
})
type?: string;
@ApiProperty({
description: 'Tags of the file',
required: false,
})
tags?: string[];
@ApiProperty({
description: 'Description of the file',
required: false,
})
description?: string;
@ApiProperty({
description: 'Filename of the file',
required: false,
})
filename?: string;
}
103 changes: 100 additions & 3 deletions src/modules/v1/localFiles/localFiles.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,33 @@ import {
BadRequestException,
Body,
Delete,
Req,
UnauthorizedException,
} from '@nestjs/common';
import LocalFilesService from './localFiles.service';
import { Response } from 'express';
import { createReadStream } from 'fs';
import { join } from 'path';
import {
ApiBadRequestResponse,
ApiBasicAuth,
ApiBody,
ApiConsumes,
ApiHeaders,
ApiOperation,
ApiPayloadTooLargeResponse,
ApiQuery,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import LocalFilesInterceptor from './localFiles.interceptor';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { ConfigService } from '@nestjs/config';
import { CreateLocalFileDto } from './dto/create-local-file.dto';
import { LocalFileFilterDto } from './dto/local-file-filter.dto';
import OwnersService from '../owners/owners.service';

const GENERAL_UPLOADS_DIR: string = './uploads/';

Expand All @@ -41,6 +50,7 @@ export default class LocalFilesController {
constructor(
private readonly localFilesService: LocalFilesService,
private readonly configService: ConfigService,
private readonly ownerService: OwnersService,
) {}
private readonly logger = new Logger(LocalFilesController.name);

Expand All @@ -52,11 +62,61 @@ export default class LocalFilesController {
description: 'Auth API key',
},
])
@ApiQuery({
name: 'type',
required: false,
type: String,
example: 'local',
enum: ['local', 'avatar'],
description: 'Type of file',
})
@ApiQuery({
name: 'tags',
required: false,
example: 'tag1,tag2',
type: [String],
description: 'Tags of the file',
})
@ApiQuery({
name: 'description',
required: false,
example: 'Description',
type: String,
description: 'Description of the file',
})
@ApiQuery({
name: 'filename',
required: false,
example: 'file.jpg',
type: String,
description: 'Filename of the file',
})
@ApiBasicAuth('api-key')
@UseGuards(AuthGuard('api-key'))
async getAllFiles() {
async getAllFiles(@Req() request: { query: LocalFileFilterDto }) {
const { query } = request;
this.logger.log(`Received a new request for all files`);
return this.localFilesService.getAllFiles();
const filters = {};
if (query.type) {
this.logger.debug(`Filtering for type active: ${query.type}`);
filters['type'] = query.type;
}
if (query.tags) {
this.logger.debug(`Filtering for tags: ${query.tags}`);
if (!Array.isArray(query.tags)) {
query.tags = [query.tags];
}
filters['tags'] = { hasSome: query.tags };
}
if (query.description) {
this.logger.debug(`Filtering for description: ${query.description}`);
filters['description'] = { contains: query.description };
}
if (query.filename) {
this.logger.debug(`Filtering for description: ${query.filename}`);
filters['filename'] = { contains: query.filename };
}
return this.localFilesService.getAllFiles(filters);
}

@Get(':id')
Expand Down Expand Up @@ -113,6 +173,8 @@ export default class LocalFilesController {
schema: {
type: 'object',
properties: {
externalId: { type: 'string' },
domain: { type: 'string' },
description: { type: 'string' },
type: { type: 'string', default: 'local' },
tags: { type: 'array' },
Expand All @@ -123,6 +185,15 @@ export default class LocalFilesController {
},
},
})
@ApiResponse({
status: 201,
description: 'The file has been successfully uploaded.',
})
@ApiBadRequestResponse({ description: 'No file uploaded' })
@ApiPayloadTooLargeResponse({ description: 'File too large' })
@ApiUnauthorizedResponse({
description: 'Unable to find or create an owner for the file',
})
@UseInterceptors(
LocalFilesInterceptor({
fieldName: 'file',
Expand Down Expand Up @@ -150,12 +221,38 @@ export default class LocalFilesController {
if (!file) {
throw new BadRequestException('No file uploaded');
}
if (!createLocalFileDto.externalId || !createLocalFileDto.domain) {
this.logger.error(
`No external id or domain provided for file: ${file.originalname}`,
);
// Remove the uploaded file
await this.localFilesService.deleteFileByPath(file.path);
throw new BadRequestException('No external id or domain provided');
}

this.logger.log(
`Received new avatar file: ${file.originalname} for id ${createLocalFileDto.externalId} and domain ${createLocalFileDto.domain}`,
);

// Retrive or create a file owner
const owner = await this.ownerService.getOwnerOrCreate({
externalId: createLocalFileDto.externalId,
domain: createLocalFileDto.domain,
});

if (!owner) {
// Remove the uploaded file
await this.localFilesService.deleteFileByPath(file.path);
throw new UnauthorizedException(
'Unable to find or create an owner for the file',
);
}

this.logger.log(`Received and saved a new file: ${file.originalname}`);
const uploaded = await this.localFilesService.addFile(file, {
description: createLocalFileDto.description,
tags: createLocalFileDto.tags,
type: createLocalFileDto.type,
ownerId: owner.id,
});
return {
...uploaded,
Expand Down
2 changes: 2 additions & 0 deletions src/modules/v1/localFiles/localFiles.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LocalFilesService from './localFiles.service';
import LocalFilesController from './localFiles.controller';
import { PrismaService } from '../../../prisma.service';
import { HttpModule } from '@nestjs/axios';
import { OwnersModule } from '../owners/owners.module';

@Module({
imports: [
Expand All @@ -17,6 +18,7 @@ import { HttpModule } from '@nestjs/axios';
inject: [ConfigService],
}),
HttpModule,
OwnersModule,
],
providers: [LocalFilesService, PrismaService],
exports: [LocalFilesService],
Expand Down
Loading

0 comments on commit dd4c939

Please sign in to comment.