DataTransferPublicServicesRest.kt| Commit | Message |
|---|---|
9ebb88522 | 2016-07-18 Initial commit |
DataTransferPublicSession), NOT Spring Security's filter chain. External users get an access token (typically a UUID in a URL parameter) that maps to a ProjectForge data transfer area. No ProjectForge user account needed — this is for sharing files with customers, suppliers, or the public.val data = dataTransferPublicSession.checkLogin(request, id)
?: return RestUtils.badRequest("No valid login.")
val area: DataTransferAreaDO = data.first // The DataTransfer area
val sessionData = data.second // Session info (userInfo, token)DataTransferPublicSession.checkLogin() is the custom auth layer. It extracts an access token from the HTTP request (likely from query params: ?accessToken=abc123) and validates it against the database. On success, it returns a Pair<DataTransferAreaDO, TransferAreaData> — the area being accessed and session metadata. On failure, returns null and the endpoint responds with 400 "No valid login."
Not a JWT or OAuth system — this is a simple token lookup. The token is generated when an admin creates a data transfer area with external access enabled. The token is embedded in the shareable URL: https://projectforge.acme.com/rsPublic/datatransfer/download/datatransfer/42?accessToken=uuid.
@RequestParam("fileIds") fileIds: String, // CSV: "abc1,def2,ghi3"
val fileIdList = fileIds.split(",")
val attachments = attachmentsService.getAttachments(...)
?.filter { attachment ->
fileIdList.any { attachment.fileId?.startsWith(it) == true }
// Match by PREFIX, not exact ID! "abc" matches "abc123def456"
}The Javadoc on line 154 explains the trick: "For preserving url length, fileIds may also be shortened (e.g. first 4 chars)." JCR file IDs are long UUIDs like "a1b2c3d4-e5f6-7890-abcd-ef1234567890". Putting multiple full UUIDs in a URL query string bloats the URL past browser limits (~2000 chars). The solution: users can send just the first 4 characters of each file ID.
How the matching works: fileIdList.any { attachment.fileId?.startsWith(it) == true } — for each attachment, check if its full fileId starts with ANY of the provided prefixes. If the user sends "a1b2,d4e5", the server matches "a1b2c3d4-..." and "d4e5f6a7-...". This is a trade-off: shorter URLs vs. risk of prefix collisions. With UUIDs, the first 4 hex chars have 16^4 = 65536 possibilities — collision risk is extremely low for typical usage (10-20 files per area).
// After upload, tell the React frontend to update the attachment list
return ResponseEntity.ok()
.body(
ResponseAction(targetType = TargetType.UPDATE, merge = true)
.addVariable("data", AttachmentsServicesRest.ResponseData(list))
)
// After delete/modify, tell React to close the modal
return ResponseEntity.ok()
.body(
ResponseAction(targetType = TargetType.CLOSE_MODAL, merge = true)
.addVariable("data", AttachmentsServicesRest.ResponseData(list))
)Instead of returning just the updated data, the endpoints return a ResponseAction — a command to the React frontend about what to do next. This is the server-driven UI pattern (SDUI) that ProjectForge uses for its dynamic layout system:
UPDATE — the frontend should update the current view with the new data (merge=true means merge into existing state, not replace)CLOSE_MODAL — the frontend should close the current modal dialog and update the parent viewThe addVariable("data", ...) attaches the new attachment list as a named variable. The frontend's dynamic layout engine reads the ResponseAction and decides which components to re-render. The server doesn't know about React components — it sends abstract actions; the frontend interprets them.
attachmentsService.addAttachment(
jcrPath, fileInfo, inputStream,
baseDao = dataTransferAreaDao,
obj = area,
accessChecker = attachmentsAccessChecker,
userString = getExternalUserString(request, sessionData.userInfo) // Audit!
)Every JCR operation passes a userString — a string identifying the external user. getExternalUserString() (from the companion object of DataTransferAreaDao) extracts the client's IP address and optional identity info from the HTTP request: "192.168.1.42 (John from Acme Corp)". This string is stored in the JCR's audit log alongside the file modification. Even though the user has no ProjectForge account, their actions are traceable — critical for GDPR compliance and security forensics.
registerFileAsOwner() (line 323): After upload, the session records which files the external user uploaded. This enables per-file ownership checks — a user can only delete/modify files they themselves uploaded, not files uploaded by other external users in the same shared area.
The convert() helper builds a DataTransferPublicArea DTO for the frontend. Two key transformations:
attachmentsAccessChecker.filterAttachments() — removes files the current external user shouldn't see. This is defense-in-depth: even if the JCR query returns all files, the access checker strips unauthorized ones before they reach the client.DataTransferUtils.expiryTimeLeft() — data transfer areas can have expiration. Each attachment gets an expiry countdown added to its DTO. The frontend can display "Expires in 3 days" or a red warning for expired files.
/download/{category}/{id}hasDownloadAccess()per-file/downloadAll/{category}/{id}externalDownloadEnabledon area/multiDownload/{category}/{id}externalDownloadEnabledon area/multiDeleteexternalUploadEnabled+ per-file ownership/upload/{category}/{id}/{listId}externalUploadEnabled/deletehasDeleteAccess()per-file/modifyEvery endpoint starts with the same 3 checks: (1) validate category =
"datatransfer", (2) validate listId =DEFAULT_NODE, (3) authenticate viadataTransferPublicSession.checkLogin(). Thecheck()function throwsIllegalStateExceptionif the condition fails — uncaught, it becomes a 500 error. Arguably, these should return 400 responses instead.