Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions frontend/src/app/models/dtos/SecureSendLinkDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface SecureSendLinkDto {
id: string;
filePath: string;
fileName: string;
shareUrl?: string;
token?: string;
expiresAt: string | Date;
hasPassword?: boolean;
isRevoked?: boolean;
revokedAt?: string | Date | null;
createdAt: string | Date;
}

export interface CreateSecureSendRequestDto {
filePath: string;
expiresAt: string;
password?: string;
}
233 changes: 232 additions & 1 deletion frontend/src/app/pages/cloud/cloud.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
tooltipPosition="bottom"
(click)="scanCurrentFolder()"
></p-button>
<p-button
label="Shared Links"
icon="pi pi-link"
[outlined]="true"
styleClass="p-button-sm cloud-accent-btn text-xxs sm:text-xs"
(click)="openSharedLinksDialog()"
pTooltip="View and manage active share links"
tooltipPosition="top"
ariaLabel="View and manage active share links"
></p-button>
<p-button
label="Trash"
icon="pi pi-trash"
Expand Down Expand Up @@ -214,6 +224,15 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
</td>
<td>
<div class="cloud-actions">
<p-button
*ngIf="entry.kind === 'file'"
icon="pi pi-share-alt"
styleClass="p-button-rounded p-button-text p-button-sm cloud-icon-btn"
(click)="openCreateShareDialog(entry.path, entry.name)"
pTooltip="Create share link"
tooltipPosition="top"
ariaLabel="Create share link"
></p-button>
<p-button
*ngIf="entry.kind === 'file'"
icon="pi pi-download"
Expand Down Expand Up @@ -634,7 +653,6 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
</div>
</ng-container>
</div>

<ng-template pTemplate="footer">
<p-button
label="Close"
Expand All @@ -650,4 +668,217 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
></p-button>
</ng-template>
</p-dialog>

<p-dialog
[(visible)]="showCreateShareDialog"
[modal]="true"
[closable]="true"
[draggable]="false"
[resizable]="false"
header="Create Secure Send Link"
styleClass="cloud-dialog"
[style]="{ width: '28rem', maxWidth: '95vw' }"
>
<div class="cloud-dialog-body" *ngIf="selectedFileForShare">
<p class="text-xxs sm:text-xs cloud-muted mb-3">
Create an expiring link to share
<strong>{{ selectedFileForShare.name }}</strong> safely.
</p>

<div class="field mb-3">
<label
for="shareExpiry"
class="text-xxs sm:text-xs font-semibold block mb-1"
>Expiration Period</label
>
<select
id="shareExpiry"
[(ngModel)]="shareExpiryMinutes"
class="p-inputtext p-component text-xxs sm:text-xs w-full"
>
<option *ngFor="let opt of expiryOptions" [value]="opt.value">
{{ opt.label }}
</option>
</select>
</div>

<div class="field mb-3">
<label
for="sharePass"
class="text-xxs sm:text-xs font-semibold block mb-1"
>Password Protection (Optional)</label
>
<input
id="sharePass"
type="password"
pInputText
[(ngModel)]="sharePassword"
placeholder="Leave empty for public access"
class="w-full text-xxs sm:text-xs"
/>
</div>
</div>
<ng-template pTemplate="footer">
<p-button
label="Cancel"
styleClass="p-button-text p-button-sm text-xxs sm:text-xs"
(click)="showCreateShareDialog = false"
></p-button>
<p-button
label="Create Link"
icon="pi pi-check"
[loading]="creatingShareLink"
styleClass="p-button-sm text-xxs sm:text-xs"
(click)="submitCreateShareLink()"
></p-button>
</ng-template>
</p-dialog>

<p-dialog
[(visible)]="showGeneratedLinkDialog"
[modal]="true"
[closable]="true"
[draggable]="false"
[resizable]="false"
header="Secure Link Created"
styleClass="cloud-dialog"
[style]="{ width: '32rem', maxWidth: '95vw' }"
>
<div class="cloud-dialog-body">
<div class="cloud-info-box mb-3 text-xxs sm:text-xs">
<i class="pi pi-exclamation-triangle"></i>
<span
><strong>Notice:</strong> This share link is displayed only once.
Please copy and save it now.</span
>
</div>
<div class="flex gap-2 align-items-center">
<input
type="text"
readonly
pInputText
[value]="createdShareUrl"
class="w-full text-xxs sm:text-xs font-mono"
/>
<p-button
icon="pi pi-copy"
label="Copy"
styleClass="p-button-sm text-xxs sm:text-xs"
(click)="copyShareUrlToClipboard()"
pTooltip="Copy link"
tooltipPosition="top"
ariaLabel="Copy link to clipboard"
></p-button>
</div>
</div>
<ng-template pTemplate="footer">
<p-button
label="Done"
styleClass="p-button-sm text-xxs sm:text-xs"
(click)="showGeneratedLinkDialog = false"
></p-button>
</ng-template>
</p-dialog>

<p-dialog
[(visible)]="showSharedLinksDialog"
[modal]="true"
[closable]="true"
[draggable]="false"
[resizable]="false"
header="Manage Secure Send Links"
styleClass="cloud-dialog"
[style]="{ width: '46rem', maxWidth: '95vw' }"
>
<div class="cloud-dialog-body">
<div
*ngIf="loadingSharedLinks"
class="cloud-state text-xxs sm:text-xs my-3"
>
<i class="pi pi-spin pi-spinner"></i> Loading share links...
</div>

<div
*ngIf="!loadingSharedLinks && sharedLinks.length === 0"
class="cloud-state text-xxs sm:text-xs my-3"
>
No active share links found.
</div>

<div
*ngIf="!loadingSharedLinks && sharedLinks.length > 0"
class="cloud-table-wrap"
>
<p-table
[value]="sharedLinks"
styleClass="text-xxs sm:text-xs"
[tableStyle]="{ 'min-width': '100%' }"
>
<ng-template pTemplate="header">
<tr>
<th>File</th>
<th>Created</th>
<th>Expires</th>
<th>Security</th>
<th>Status</th>
<th class="text-right">Action</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-link>
<tr>
<td>{{ link.fileName }}</td>
<td class="cloud-muted">{{ link.createdAt | date: "short" }}</td>
<td class="cloud-muted">{{ link.expiresAt | date: "short" }}</td>
<td>
<span
class="cloud-badge"
[class.badge-protected]="link.hasPassword"
[class.badge-public]="!link.hasPassword"
>
<i
class="pi"
[class.pi-lock]="link.hasPassword"
[class.pi-unlock]="!link.hasPassword"
></i>
{{ link.hasPassword ? "Password" : "Public" }}
</span>
</td>
<td>
<span
class="cloud-badge"
[class.badge-revoked]="link.isRevoked"
[class.badge-active]="!link.isRevoked"
>
{{ link.isRevoked ? "Revoked" : "Active" }}
</span>
</td>
<td class="text-right">
<p-button
*ngIf="!link.isRevoked"
icon="pi pi-ban"
severity="danger"
styleClass="p-button-rounded p-button-text p-button-sm"
[loading]="revokingLinkId === link.id"
(click)="revokeShareLink(link)"
pTooltip="Revoke share link"
tooltipPosition="top"
ariaLabel="Revoke share link"
></p-button>
<span *ngIf="link.isRevoked" class="cloud-muted text-xxs"
>Revoked</span
>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<ng-template pTemplate="footer">
<p-button
label="Close"
styleClass="p-button-text p-button-sm text-xxs sm:text-xs"
(click)="showSharedLinksDialog = false"
></p-button>
</ng-template>
</p-dialog>
</div>
51 changes: 51 additions & 0 deletions frontend/src/app/pages/cloud/cloud.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -786,3 +786,54 @@
word-break: break-all;
white-space: pre-wrap;
}

/* Secure Send Styles */
.cloud-info-box {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.375rem;
color: #d97706;

i {
font-size: 1rem;
margin-top: 0.1rem;
}
}

.cloud-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.15rem 0.5rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 500;

&.badge-protected {
background: rgba(99, 102, 241, 0.15);
color: #6366f1;
border: 1px solid rgba(99, 102, 241, 0.3);
}

&.badge-public {
background: rgba(107, 114, 128, 0.15);
color: #9ca3af;
border: 1px solid rgba(107, 114, 128, 0.3);
}

&.badge-active {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}

&.badge-revoked {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
}
Loading