Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8894742
Merge pull request #5 from venu123143/refresh_tokens
venu123143 Dec 7, 2025
c535a2b
Merge pull request #6 from venu123143/refresh_tokens
venu123143 Dec 7, 2025
d31ce20
Merge pull request #7 from venu123143/refresh_tokens
venu123143 Dec 26, 2025
11f0207
Merge pull request #9 from venu123143/refresh_tokens
venu123143 Dec 28, 2025
496469e
revoke share api changes
venu123143 Jan 1, 2026
5af0973
revoke access changes
venu123143 Jan 1, 2026
d667fa5
Merge pull request #10 from venu123143/refresh_tokens
venu123143 Jan 3, 2026
0193222
backend changes for the dashboard
venu123143 Jan 10, 2026
18a8fd1
Merge pull request #11 from venu123143/refresh_tokens
venu123143 Jan 10, 2026
a9c736c
get api changes
venu123143 Feb 21, 2026
498699d
Merge pull request #12 from venu123143/refresh_tokens
venu123143 Feb 21, 2026
0763c7b
bakend get all files api changes
venu123143 Feb 22, 2026
ae442f7
Merge pull request #13 from venu123143/refresh_tokens
venu123143 Feb 22, 2026
7065193
commented the with ip code
venu123143 Feb 22, 2026
8b7a905
edn url changes
venu123143 Mar 7, 2026
9bdf819
code changes
venu123143 Mar 7, 2026
c777db1
Merge pull request #14 from venu123143/refresh_tokens
venu123143 Mar 7, 2026
7b41d67
docker compose changes
venu123143 Apr 18, 2026
9239757
Merge pull request #15 from venu123143/refresh_tokens
venu123143 Apr 18, 2026
09bb41c
ci-cd modified
venu123143 Apr 18, 2026
d39fb9f
Merge pull request #16 from venu123143/refresh_tokens
venu123143 Apr 18, 2026
baf0acc
conflicts resolved
venu123143 May 16, 2026
9d251f1
backend api changes
venu123143 May 16, 2026
99d5c7e
Merge pull request #17 from venu123143/refresh_tokens
venu123143 May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/development'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
Expand All @@ -27,10 +32,12 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
provenance: false
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKER_IMAGE }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }}
Expand All @@ -43,17 +50,15 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/development'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -e
cd /var/www/html/file-flow-backend
docker-compose down
docker-compose pull
docker-compose up -d
docker compose pull
docker compose up -d
docker system prune -f

# Run migrations via script
./migrate.sh
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ pids
*.pid
*.seed
*.pid.lock

*.md
*.txt
Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Ignoring all .md and .txt files will block essential documentation.

The patterns *.md and *.txt will prevent version control of README.md, CONTRIBUTING.md, CHANGELOG.md, and other important documentation. This will block documentation commits and violate repository best practices.

If you need to ignore specific markdown/text files (e.g., generated reports or local notes), use more specific patterns instead:

🔧 Suggested fix: use specific patterns
-*.md
-*.txt
+# Ignore only generated/local documentation
+/docs/generated/*.md
+/reports/*.txt
+notes.md
+TODO.txt

Or if these are truly temporary local files, add them to your personal .git/info/exclude instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 17 - 18, Remove the overly-broad ignore patterns
'*.md' and '*.txt' from .gitignore and replace them with specific patterns or
explicit exclusions: delete the two lines, add narrow ignores for only generated
or local files (e.g., build/*.md, reports/*.txt) or add explicit allow rules for
important docs (e.g., !README.md, !CONTRIBUTING.md, !CHANGELOG.md) so
documentation remains tracked; if those patterns were intended only for personal
temp files, move them to your local .git/info/exclude instead.

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

Expand Down
16 changes: 11 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# ============================
# Single-stage Bun build
# Single-stage Bun build (multi-arch: amd64 + arm64)
# ============================
FROM oven/bun:1.2.21-debian
ARG TARGETPLATFORM
FROM --platform=$TARGETPLATFORM oven/bun:1.2.21-debian

Check warning on line 5 in Dockerfile

View workflow job for this annotation

GitHub Actions / build-and-push

Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior

RedundantTargetPlatform: Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior More info: https://docs.docker.com/go/dockerfile/rule/redundant-target-platform/

WORKDIR /app

# Install curl for the HEALTHCHECK (not present in the base image)
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Copy package files and Bun config
COPY package.json bun.lock bunfig.toml ./

# Install all dependencies (dev + prod)
RUN bun install
# Install all dependencies (dev + prod) using the lockfile for reproducible arm64 builds
RUN bun install --frozen-lockfile

# Copy the rest of the source code
COPY . .
Expand All @@ -25,4 +31,4 @@
CMD curl -f http://localhost:7000/health || exit 1

# Start Bun directly with TypeScript
CMD ["bun", "run" , "src/index.ts"]
CMD ["bun", "run", "src/index.ts"]
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface Config {
ENDPOINT: string;
BUCKET_NAME: string;
REGION: string;
CDN_URL: string;
};
CLOUDFLARE: {
CDN_DOMAIN: string;
Expand Down Expand Up @@ -78,6 +79,7 @@ const config: Config = {
ENDPOINT: process.env.S3_ENDPOINT!,
BUCKET_NAME: process.env.S3_BUCKET_NAME!,
REGION: process.env.S3_REGION!,
CDN_URL: process.env.S3_CDN_URL!,
},
CLOUDFLARE: {
CDN_DOMAIN: process.env.CLOUDFLARE_CDN_DOMAIN!,
Expand Down
115 changes: 115 additions & 0 deletions src/config/s3-secondary.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
S3Client,
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
ListObjectsV2Command,
type PutObjectCommandInput,
HeadObjectCommand,
} from "@aws-sdk/client-s3";
import { FolderNameEnum } from "@/config/s3.config";

// export enum SecondaryFolderNameEnum {
// FILES = "files",
// VIDEOS = "videos",
// IMAGES = "images",
// DOCUMENTS = "documents",
// RECORDINGS = "recordings"
// }

export class SecondaryS3Service {
private s3Client: S3Client;
private bucketName: string;

constructor() {
this.bucketName = process.env.SECONDARY_S3_BUCKET_NAME!;

this.s3Client = new S3Client({
endpoint: process.env.SECONDARY_S3_ENDPOINT!,
region: process.env.SECONDARY_S3_REGION || 'auto',
credentials: {
accessKeyId: process.env.SECONDARY_S3_TOKEN_ID!,
secretAccessKey: process.env.SECONDARY_S3_SECRET_KEY!,
},
forcePathStyle: true,
requestHandler: {
requestTimeout: 300000, // 5 minutes timeout
} as any,
});
}

public async listFiles(folder?: FolderNameEnum, maxKeys: number = 1000, continuationToken?: string) {
const command = new ListObjectsV2Command({
Bucket: this.bucketName,
Prefix: folder ? `${folder}/` : undefined,
MaxKeys: maxKeys,
ContinuationToken: continuationToken,
});

const response = await this.s3Client.send(command);
return {
files: response.Contents?.map(obj => ({
key: obj.Key!,
size: obj.Size!,
lastModified: obj.LastModified!,
etag: obj.ETag!,
})) || [],
isTruncated: response.IsTruncated || false,
nextContinuationToken: response.NextContinuationToken,
};
}

public async getFile(key: string): Promise<Buffer> {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});

const response = await this.s3Client.send(command);

if (!response.Body) {
throw new Error(`No body returned for key: ${key}`);
}

// Convert stream to buffer
const chunks: Uint8Array[] = [];
for await (const chunk of response.Body as any) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}

public async getFileStream(key: string) {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});

const response = await this.s3Client.send(command);

if (!response.Body) {
throw new Error(`No body returned for key: ${key}`);
}

return response.Body;
}

public async getMetadata(key: string) {
const command = new HeadObjectCommand({
Bucket: this.bucketName,
Key: key,
});
const response = await this.s3Client.send(command);
return {
contentType: response.ContentType,
contentLength: response.ContentLength,
lastModified: response.LastModified,
metadata: response.Metadata,
etag: response.ETag,
};
}
}

export default new SecondaryS3Service();

75 changes: 71 additions & 4 deletions src/config/s3.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,20 @@ export class S3Service {
}

public buildCDNUrl(key: string): string {
return `https://${config.CLOUDFLARE.CDN_DOMAIN}/${key}`;
return `${config.S3.CDN_URL}/${key}`;
}

/**
* Sanitizes metadata values to ensure they're valid for HTTP headers.
* HTTP headers cannot contain certain characters like non-ASCII, control characters, etc.
*/
private sanitizeMetadataValue(value: string): string {
// Replace non-ASCII and control characters with safe equivalents
// Keep only alphanumeric, spaces, hyphens, underscores, and periods
return value
.replace(/[^\x20-\x7E]/g, '') // Remove non-printable ASCII
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_') // Replace invalid chars with underscore
.substring(0, 2000); // AWS metadata value limit is 2KB
}

private calculateChunkChecksum(chunkBuffer: Buffer<ArrayBuffer>): string {
Expand All @@ -63,13 +76,16 @@ export class S3Service {
}

public async uploadFile(file: IFile): Promise<string> {
// Sanitize original name for HTTP header compatibility
const sanitizedOriginalName = this.sanitizeMetadataValue(file.originalName);

const params: PutObjectCommandInput = {
Bucket: config.S3.BUCKET_NAME,
Key: `${FolderNameEnum.FILES}/${file.filename}`,
Body: file.buffer,
ContentType: file.mimetype,
Metadata: {
originalName: file.originalName,
originalName: sanitizedOriginalName,
uploadedAt: new Date().toISOString(),
size: file.size.toString(),
},
Expand All @@ -88,13 +104,16 @@ export class S3Service {
folder: FolderNameEnum = FolderNameEnum.FILES
): Promise<string> {
const path = `${folder}/${key}`;
// Sanitize original name for HTTP header compatibility
const sanitizedOriginalName = this.sanitizeMetadataValue(fileName);

const params = {
Bucket: config.S3.BUCKET_NAME,
Key: path,
Body: buffer,
ContentType: mimeType,
Metadata: {
originalName: fileName,
originalName: sanitizedOriginalName,
uploadedAt: new Date().toISOString(),
size: buffer.length.toString(),
},
Expand All @@ -105,6 +124,35 @@ export class S3Service {
return this.buildCDNUrl(path);
}

public async uploadStream(
key: string,
stream: any,
fileName: string,
mimeType: string,
size: number,
folder: FolderNameEnum = FolderNameEnum.FILES
): Promise<string> {
const path = `${folder}/${key}`;
const sanitizedOriginalName = this.sanitizeMetadataValue(fileName);

const params = {
Bucket: config.S3.BUCKET_NAME,
Key: path,
Body: stream,
ContentType: mimeType,
ContentLength: size, // Required for streaming
Metadata: {
originalName: sanitizedOriginalName,
uploadedAt: new Date().toISOString(),
size: size.toString(),
},
CacheControl: 'public, max-age=31536000',
};

await this.s3Client.send(new PutObjectCommand(params));
return this.buildCDNUrl(path);
}

public async getFileUrl(key: string, expiresIn: number = 3600): Promise<string> {
const command = new GetObjectCommand({
Bucket: config.S3.BUCKET_NAME,
Expand Down Expand Up @@ -163,13 +211,32 @@ export class S3Service {
size: obj.Size!,
lastModified: obj.LastModified!,
etag: obj.ETag!,
cdnUrl: this.buildCDNUrl(obj.Key!)
storageClass: obj.StorageClass,
owner: obj.Owner ? {
displayName: obj.Owner.DisplayName,
id: obj.Owner.ID
} : undefined,
cdnUrl: this.buildCDNUrl(obj.Key!),
// Include all other S3 properties
...(obj as any)
})) || [],
isTruncated: response.IsTruncated || false,
nextContinuationToken: response.NextContinuationToken,
keyCount: response.KeyCount,
maxKeys: response.MaxKeys,
prefix: response.Prefix,
delimiter: response.Delimiter,
encodingType: response.EncodingType,
commonPrefixes: response.CommonPrefixes,
};
}

public async getAllFiles(folder?: string, maxKeys: number = 100, continuationToken?: string) {
// Convert string folder name to FolderNameEnum if provided
const folderEnum = folder ? (folder as FolderNameEnum) : undefined;
return await this.listFiles(folderEnum, maxKeys, continuationToken);
}

public async initiateMultipartUpload(fileName: string, mimeType: string): Promise<{ uploadId: string | undefined; key: string }> {
const safeName = slugify(fileName, {
replacement: "_", // replace invalid chars with underscore
Expand Down
Loading
Loading