Conversation
Signed-off-by: Philip Miglinci <pmig@glasskube.com>
Signed-off-by: Philip Miglinci <pmig@glasskube.com>
There was a problem hiding this comment.
Pull request overview
Introduces a new “public links” sharing feature across the stack (OpenAPI + Go backend + DB schema + Angular UI), enabling authenticated users to create/manage share links and unauthenticated viewers to browse/download shared dateien via an access token (optionally protected by a code).
Changes:
- Added Link domain (event-sourced) with projections, SQLC queries, and server endpoints for link CRUD + scope-checked public access.
- Extended OpenAPI spec and regenerated Go/Angular API bindings for link operations and public link browsing/downloading.
- Added Angular UI for link management and a public link viewer route, plus dashboard actions to create/add items to links.
Reviewed changes
Copilot reviewed 76 out of 76 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/api/zz_generated.models.go | Adds OpenAPI-generated models/params/enums for links and public-link error handling. |
| internal/server/zz_generated.server.go | Adds generated server interface/routes/response objects for link and public link endpoints. |
| internal/server/server.go | Wires link.Service into the main server via new repo dependency. |
| internal/server/endpoints_public_links.go | Implements public (unauthenticated) list/download endpoints for links. |
| internal/server/endpoints_links.go | Implements authenticated link management endpoints (list/create/update/revoke/rotate/add/remove). |
| internal/link/service.go | Implements link business logic (owner operations + public access verification/scope checks). |
| internal/link/repository.go | Adds link repository backed by generic event-sourcing repository + projection updates. |
| internal/link/projections.go | Projection updaters for link events (link_projection + link_datei_projection). |
| internal/link/mapping.go | Maps link projections/aggregates into API Link responses (including counts/dateien). |
| internal/link/events.go | Defines link domain events + registers them for deserialization; adds link event store wiring. |
| internal/link/aggregate.go | Defines link aggregate commands/state and name validation. |
| internal/db/zz_generated.models.go | Adds SQLC-generated DB models for link tables. |
| internal/db/zz_generated_schema.sql | Updates schema snapshot used by SQLC generation to include link tables. |
| internal/db/sqlc.yaml | Registers new SQL query files (link + link_event) for SQLC generation. |
| internal/db/migrations/sql/0_initial_schema.up.sql | Adds link_event store, link_projection, and link_datei_projection schema + indexes/constraints. |
| internal/db/migrations/sql/0_initial_schema.down.sql | Drops link tables on rollback. |
| internal/db/link.sql.go | SQLC-generated Go queries for link projections, scope checks, and content counts. |
| internal/db/link.sql | SQL queries for link projections, scope checks, and content counts. |
| internal/db/link_event.sql.go | SQLC-generated Go queries for link event store operations. |
| internal/db/link_event.sql | SQL queries for link event store operations. |
| internal/db/datei.sql.go | Adds SQLC-generated helper query CountDateiProjectionsByIDs. |
| internal/db/datei.sql | Adds CountDateiProjectionsByIDs query to validate link datei IDs. |
| internal/dateierrors/errors.go | Adds new link/public-share error sentinels. |
| internal/cmd/serve/serve.go | Wires link event store/repository into server startup. |
| frontend/src/util/format-bytes.ts | Adds byte formatting utility for public link viewer. |
| frontend/src/util/download.ts | Adds a shared “trigger browser download” helper. |
| frontend/src/app/services/links.service.ts | Adds Angular service wrapping generated API calls for link operations and public viewer calls. |
| frontend/src/app/services/auth.service.ts | Excludes /api/v1/public from authenticated-route logic so token interceptor doesn’t attach auth. |
| frontend/src/app/public-links/public-link-viewer/public-link-viewer.component.ts | Adds public link viewer component logic (unlock code flow, listing, navigation, download). |
| frontend/src/app/public-links/public-link-viewer/public-link-viewer.component.html | Adds viewer UI for public link browsing and code entry. |
| frontend/src/app/nav/nav.component.ts | Adds RouterLinkActive import for nav highlighting. |
| frontend/src/app/nav/nav.component.html | Adds nav entries for Files + Links using routerLink/routerLinkActive. |
| frontend/src/app/links/links-list/links-list.component.ts | Adds authenticated “Links” page listing active/expired/revoked links with actions. |
| frontend/src/app/links/links-list/links-list.component.html | Adds links table UI with share/copy/edit/rotate/revoke actions. |
| frontend/src/app/links/link-picker-dialog/link-picker-dialog.component.ts | Adds dialog to pick an existing link to add selected dateien into. |
| frontend/src/app/links/link-picker-dialog/link-picker-dialog.component.html | Adds picker dialog UI. |
| frontend/src/app/links/link-form-dialog/link-form-dialog.component.ts | Adds create/edit dialog for link name/code/expiry and removing shared items. |
| frontend/src/app/links/link-form-dialog/link-form-dialog.component.html | Adds create/edit dialog UI. |
| frontend/src/app/dashboard/dashboard.component.ts | Adds dashboard actions to create links / add selection to links; uses new download helper. |
| frontend/src/app/dashboard/dashboard.component.html | Adds selection toolbar + row menu entries for public link actions. |
| frontend/src/app/app.routes.ts | Adds public route /share/:accessToken to load public link viewer. |
| frontend/src/app/app-logged-in.routes.ts | Adds authenticated route /links to load links list page. |
| frontend/src/api/models/update-link-request.ts | Generated Angular model for UpdateLinkRequest. |
| frontend/src/api/models/public-link-error-response.ts | Generated Angular model for public link 403 error payload. |
| frontend/src/api/models/list-public-link-dateien-response.ts | Generated Angular model for public public link listing response. |
| frontend/src/api/models/list-links-response.ts | Generated Angular model for authenticated link listing response. |
| frontend/src/api/models/link.ts | Generated Angular model for Link. |
| frontend/src/api/models/create-link-request.ts | Generated Angular model for CreateLinkRequest. |
| frontend/src/api/models/add-datei-to-link-request.ts | Generated Angular model for AddDateiToLinkRequest. |
| frontend/src/api/models.ts | Exports new generated Angular models. |
| frontend/src/api/functions.ts | Exports new generated Angular API functions for link operations. |
| frontend/src/api/fn/operations/update-link.ts | Generated Angular client for PATCH /api/v1/links/{id}. |
| frontend/src/api/fn/operations/rotate-link-access-token.ts | Generated Angular client for POST /api/v1/links/{id}/rotate. |
| frontend/src/api/fn/operations/revoke-link.ts | Generated Angular client for DELETE /api/v1/links/{id}. |
| frontend/src/api/fn/operations/remove-datei-from-link.ts | Generated Angular client for DELETE /api/v1/links/{id}/dateien/{dateiId}. |
| frontend/src/api/fn/operations/list-public-link-dateien.ts | Generated Angular client for GET /api/v1/public/links/{accessToken}/dateien. |
| frontend/src/api/fn/operations/list-links.ts | Generated Angular client for GET /api/v1/links. |
| frontend/src/api/fn/operations/download-public-link-datei.ts | Generated Angular client for GET public download endpoint. |
| frontend/src/api/fn/operations/create-link.ts | Generated Angular client for POST /api/v1/links. |
| frontend/src/api/fn/operations/add-datei-to-link.ts | Generated Angular client for POST /api/v1/links/{id}/dateien. |
| api/paths/public_links_token_dateien.yaml | Adds OpenAPI path for public listing endpoint. |
| api/paths/public_links_token_dateien_dateiid_download.yaml | Adds OpenAPI path for public download endpoint. |
| api/paths/links.yaml | Adds OpenAPI path for link list/create endpoints. |
| api/paths/links_id.yaml | Adds OpenAPI path for link update/revoke endpoints. |
| api/paths/links_id_rotate.yaml | Adds OpenAPI path for link access token rotation. |
| api/paths/links_id_dateien.yaml | Adds OpenAPI path for adding a datei to a link. |
| api/paths/links_id_dateien_dateiid.yaml | Adds OpenAPI path for removing a datei from a link. |
| api/openapi.yaml | Registers new authenticated and public link routes in the OpenAPI spec. |
| api/components/schemas/UpdateLinkRequest.yaml | Adds schema for UpdateLinkRequest. |
| api/components/schemas/PublicLinkErrorResponse.yaml | Adds schema for public link code errors. |
| api/components/schemas/ListPublicLinkDateienResponse.yaml | Adds schema for public link listing response. |
| api/components/schemas/ListLinksResponse.yaml | Adds schema for link list response. |
| api/components/schemas/Link.yaml | Adds schema for Link object. |
| api/components/schemas/CreateLinkRequest.yaml | Adds schema for CreateLinkRequest. |
| api/components/schemas/AddDateiToLinkRequest.yaml | Adds schema for AddDateiToLinkRequest. |
| AGENTS.md | Updates contributor guidelines (no empty/placeholder CSS/SCSS files). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return DownloadPublicLinkDatei200ApplicationoctetStreamResponse{ | ||
| Body: result.Reader, | ||
| Headers: DownloadPublicLinkDatei200ResponseHeaders{ | ||
| ContentDisposition: fmt.Sprintf(`attachment; filename="%v"`, result.ContentFileName), | ||
| ContentType: result.ContentType, | ||
| }, |
| <ng-container matColumnDef="actions" stickyEnd> | ||
| <th mat-header-cell *matHeaderCellDef></th> | ||
| <td mat-cell *matCellDef="let row" class="w-12 text-right"> | ||
| <button matIconButton (click)="download(row)" aria-label="Open or download"> | ||
| <mat-icon>{{ row.isDirectory ? 'arrow_forward' : 'download' }}</mat-icon> | ||
| </button> | ||
| </td> | ||
| </ng-container> | ||
|
|
||
| <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | ||
| <tr | ||
| mat-row | ||
| *matRowDef="let row; columns: displayedColumns" | ||
| (click)="download(row)" | ||
| class="cursor-pointer" | ||
| ></tr> |
| func updateProjectionForLinkDateiRemoved(ctx context.Context, q *db.Queries, event *LinkDateiRemovedEvent) error { | ||
| if err := q.DeleteLinkDateiProjection(ctx, db.DeleteLinkDateiProjectionParams{ | ||
| LinkID: event.ID, | ||
| DateiID: event.DateiID, | ||
| }); err != nil { | ||
| return fmt.Errorf("failed to delete link_datei_projection: %w", err) | ||
| } | ||
| return nil |
| func (s *Service) ListLinks(ctx context.Context) (*ListLinksOutput, error) { | ||
| userID := authn.RequireContext(ctx).UserID | ||
| queries := db.New(s.db) | ||
|
|
||
| projections, err := queries.ListLinkProjectionsByOwner(ctx, userID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| items := make([]api.Link, 0, len(projections)) | ||
| for i := range projections { | ||
| dateien, err := queries.ListDateienByLink(ctx, projections[i].ID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| counts, err := queries.CountLinkContents(ctx, projections[i].ID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| mapped := MapProjectionToAPI(&projections[i], dateien, int(counts.FileCount), int(counts.FolderCount)) | ||
| if mapped != nil { | ||
| items = append(items, *mapped) | ||
| } | ||
| } |
| func (s *Service) RemoveDateiFromLink(ctx context.Context, linkID, dateiID uuid.UUID) error { | ||
| userID := authn.RequireContext(ctx).UserID | ||
|
|
||
| agg, err := s.loadOwnedAggregate(ctx, linkID, userID) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := agg.RemoveDatei(dateiID, time.Now()); err != nil { | ||
| return dateierrors.ErrLinkDateiNotShared | ||
| } | ||
| return s.repository.Save(ctx, agg) |
| ref.afterClosed().subscribe((link) => { | ||
| if (!link) return; | ||
| const shareUrl = this.linksService.buildShareUrl(link.accessToken); | ||
| const snackRef = this.snackBar.open(`Public link "${link.name}" created`, 'Copy link', { | ||
| duration: 6000, | ||
| }); | ||
| snackRef.onAction().subscribe(() => { | ||
| void navigator.clipboard.writeText(shareUrl); | ||
| }); |
| if (v.code.trim() !== '') { | ||
| body.code = v.code; | ||
| } else { | ||
| body.clearCode = true; | ||
| } |
Signed-off-by: Philip Miglinci <pmig@glasskube.com>
| schema: | ||
| type: object | ||
| properties: | ||
| message: | ||
| type: string | ||
| required: | ||
| - message |
There was a problem hiding this comment.
should be in schemas maybe, also this is the first time we need a schema for an error
| protected readonly listResource = resource({ | ||
| params: () => ({}), | ||
| loader: () => firstValueFrom(this.linksService.listLinks()), | ||
| }); | ||
|
|
||
| // Only active links accept new dateien — revoked links are terminal. | ||
| protected readonly availableLinks = computed(() => | ||
| (this.listResource.value() ?? []).filter((l) => !l.revokedAt), | ||
| ); |
There was a problem hiding this comment.
I think this filtering can be done in the resource
| const ms = expiresAt.getTime() - Date.now(); | ||
| if (ms <= 0) return 'Expired'; | ||
| const minutes = Math.floor(ms / 60_000); | ||
| const hours = Math.floor(minutes / 60); | ||
| const days = Math.floor(hours / 24); | ||
| if (days >= 30) return `Expires on ${expiresAt.toLocaleDateString()}`; | ||
| if (days >= 1) return `Expires in ${days} ${days === 1 ? 'day' : 'days'}`; | ||
| if (hours >= 1) return `Expires in ${hours} ${hours === 1 ? 'hour' : 'hours'}`; | ||
| if (minutes >= 1) return `Expires in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`; | ||
| return 'Expires in less than a minute'; |
There was a problem hiding this comment.
should definitely use a library for this. we used dayjs and datefns in the past. datefns should be preferred because it uses a more modular and modern architecture
| >chevron_right</mat-icon | ||
| > | ||
| } | ||
| <button mat-button (click)="navigateToBreadcrumb(i)" [disabled]="last"> |
There was a problem hiding this comment.
I think it looks strange that the last item is always grey. especially if there is only one item it looks very strange
…blic-sharing-links Signed-off-by: Philip Miglinci <pmig@glasskube.com>
Signed-off-by: Philip Miglinci <pmig@glasskube.com>
…blic-sharing-links Signed-off-by: Philip Miglinci <pmig@glasskube.com>
229ddaf to
8675c06
Compare
No description provided.