Skip to content

Conversation

@gugu
Copy link
Contributor

@gugu gugu commented Jan 23, 2026

No description provided.

gugu and others added 2 commits January 23, 2026 13:06
- Replace timestamp-based file keys with cryptographically secure nanoid
- Return previewUrl from upload endpoint for immediate preview
- Fix "File key not found in row" error when uploading to new records

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace Observable-based API with Promise-based async/await pattern
for better integration with Angular signals. Update all S3 components
and tests accordingly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 23, 2026 21:35
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors the S3 widget implementation to use secure, randomized file paths and modernizes the frontend architecture with Angular signals.

Changes:

  • Backend file key generation changed from timestamp-based to nanoid-based randomized keys for enhanced security
  • Frontend S3 service converted from Observable-based to Promise-based async/await pattern
  • S3 edit component migrated from class-based inputs to Angular signal-based reactive architecture
  • Added previewUrl to upload response for immediate preview access after file upload

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
backend/src/entities/s3-widget/s3-helper.service.ts Implements nanoid-based file key generation with extension validation, replacing timestamp-based naming
backend/src/entities/s3-widget/use-cases/get-s3-upload-url.use.case.ts Adds previewUrl generation during upload URL creation
backend/src/entities/s3-widget/application/data-structures/s3-operation.ds.ts Extends S3UploadUrlResponseDs with previewUrl field
frontend/src/app/services/s3.service.ts Converts from Observable pattern to async/await with Promises, adds combined uploadFile method
frontend/src/app/services/s3.service.spec.ts Updates tests for async/await pattern using fakeAsync and flush
frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.ts Migrates to signal-based inputs/outputs, removes BaseEditFieldComponent inheritance
frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.spec.ts Comprehensive test updates for signal-based component architecture
frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.html Template updates to use signal getters with function call syntax
frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.ts Converts to async/await pattern for getFileUrl calls
frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.ts Converts to async/await pattern for getFileUrl calls

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to 113
// Initialize internal value from input
const initialValue = this.value();
if (initialValue) {
this.internalValue.set(initialValue);
this._loadPreview();
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

In ngOnInit, _loadPreview() is called directly (line 112) if initialValue exists. Since this is also an async function, this call is not awaited, which means the component initialization continues before the preview loads. While this might be intentional for performance, if _loadPreview() throws an error, it will become an unhandled promise rejection. Consider adding error handling or using async ngOnInit pattern if synchronous initialization is needed.

Copilot uses AI. Check for mistakes.
uploadUrl: string;
key: string;
expiresIn: number;
previewUrl: string;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The S3UploadUrlResponseDs now includes a previewUrl field (line 30), but the API documentation in the controller (lines 123-139) doesn't document this new response field. API consumers won't know about this field without updated documentation. Consider adding API response schema documentation using @apiresponse decorator with proper schema definition to document the complete response structure including the new previewUrl field.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +53
public generateFileKey(prefix: string | undefined, filename: string): string {
const id = nanoid(12);
const extension = this._extractFileExtension(filename);
const key = extension ? `${id}${extension}` : id;

if (prefix) {
const normalizedPrefix = prefix.replace(/\/$/, '');
return `${normalizedPrefix}/${key}`;
}
return key;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The generateFileKey method now generates file keys using nanoid(12) instead of timestamp-based names. This is a breaking change that could affect existing S3 file paths and references. Files uploaded before this change will have keys like "1234567890_filename.pdf" while new files will have keys like "abc123xyz456.pdf". This inconsistency could cause issues with file organization, searching, and debugging. Consider the impact on existing data and whether a migration or versioning strategy is needed.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +53
public generateFileKey(prefix: string | undefined, filename: string): string {
const id = nanoid(12);
const extension = this._extractFileExtension(filename);
const key = extension ? `${id}${extension}` : id;

if (prefix) {
const normalizedPrefix = prefix.replace(/\/$/, '');
return `${normalizedPrefix}/${key}`;
}
return key;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The new file key generation removes the original filename from the S3 key (except for the extension). This means stored keys like "abc123xyz456.pdf" provide no human-readable information about the file contents, making it harder to identify files when browsing S3 directly, during debugging, or when reviewing logs. The previous timestamp-based approach with sanitized filenames (e.g., "1234567890_my_document.pdf") was more traceable. Consider preserving some portion of the original filename for debugging and operational purposes.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +65
public generateFileKey(prefix: string | undefined, filename: string): string {
const id = nanoid(12);
const extension = this._extractFileExtension(filename);
const key = extension ? `${id}${extension}` : id;

if (prefix) {
const normalizedPrefix = prefix.replace(/\/$/, '');
return `${normalizedPrefix}/${key}`;
}
return key;
}

private _extractFileExtension(filename: string): string {
const lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex === -1 || lastDotIndex === 0) {
return '';
}
const extension = filename.slice(lastDotIndex).toLowerCase();
if (/^\.[a-z0-9]+$/i.test(extension)) {
return extension;
}
return '';
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The generateFileKey and _extractFileExtension methods in S3HelperService lack test coverage. Given that this is critical functionality that affects how files are stored in S3 and includes security-related filename validation, comprehensive tests should be added to verify correct behavior for various filename patterns, edge cases, and security scenarios (e.g., path traversal attempts, invalid characters, etc.).

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 113
ngOnInit(): void {
super.ngOnInit();
this.connectionId = this.connectionsService.currentConnectionID;
this.tableName = this.tablesService.currentTableName;
this._parseWidgetParams();
if (this.value) {
this._loadPreview();
}
}

ngOnChanges(): void {
this._parseWidgetParams();
if (this.value && !this.previewUrl && !this.isLoading) {
// Initialize internal value from input
const initialValue = this.value();
if (initialValue) {
this.internalValue.set(initialValue);
this._loadPreview();
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

Both the effect (lines 95-101) and ngOnInit (lines 108-113) can trigger _loadPreview() for the same initial value. If a component is initialized with a value and rowPrimaryKey, ngOnInit calls _loadPreview() at line 112. The effect will also trigger because value() and rowPrimaryKey() are set, calling _loadPreview() again. This results in two concurrent HTTP requests for the same preview URL, wasting resources and potentially causing race conditions. Consider either removing the _loadPreview() call from ngOnInit or adding a flag to prevent duplicate requests.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 13
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
import { HttpException } from '@nestjs/common/exceptions/http.exception.js';
import JSON5 from 'json5';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { WidgetTypeEnum } from '../../../enums/index.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
import { S3GetUploadUrlDs, S3UploadUrlResponseDs } from '../application/data-structures/s3-operation.ds.js';
import { S3WidgetParams } from '../application/data-structures/s3-widget-params.ds.js';
import { S3HelperService } from '../s3-helper.service.js';
import { IGetS3UploadUrl } from './s3-use-cases.interface.js';
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The import statements for WidgetTypeEnum and JSON5 have been reordered (moved from lines 14-15 to lines 3 and 7). While this doesn't affect functionality, it breaks the apparent import organization convention where external library imports (JSON5) come before internal imports. This makes the import section less readable.

Copilot uses AI. Check for mistakes.
@@ -1,23 +1,24 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { Component, computed, effect, inject, input, OnInit, output, signal } from '@angular/core';
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The S3EditComponent has been changed from extending BaseEditFieldComponent to implementing its own signal-based inputs. However, all other edit field components in the codebase still extend BaseEditFieldComponent (see frontend/src/app/components/ui-components/record-edit-fields/*/component.ts files). This creates an inconsistency in the codebase architecture. Consider either keeping the BaseEditFieldComponent inheritance or establishing a clear pattern for when to use signals vs class-based inputs across all edit field components.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +65
private _extractFileExtension(filename: string): string {
const lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex === -1 || lastDotIndex === 0) {
return '';
}
const extension = filename.slice(lastDotIndex).toLowerCase();
if (/^\.[a-z0-9]+$/i.test(extension)) {
return extension;
}
return '';
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The _extractFileExtension method only validates extensions matching /^.[a-z0-9]+$/i pattern. This will reject valid file extensions containing multiple dots (e.g., ".tar.gz", ".min.js") or other characters (e.g., ".c++"). While these might be edge cases, rejecting them silently by returning an empty string could cause confusion when users upload such files and they lose their extension.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +53
public generateFileKey(prefix: string | undefined, filename: string): string {
const id = nanoid(12);
const extension = this._extractFileExtension(filename);
const key = extension ? `${id}${extension}` : id;

if (prefix) {
const normalizedPrefix = prefix.replace(/\/$/, '');
return `${normalizedPrefix}/${key}`;
}
return key;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The generateFileKey method doesn't sanitize or validate the prefix parameter before using it in the S3 key. A malicious or misconfigured prefix containing path traversal sequences (e.g., "../") or special characters could lead to files being written to unexpected S3 locations. Consider validating the prefix to ensure it doesn't contain path traversal patterns or other potentially dangerous characters.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants