Skip to content

Commit 6356363

Browse files
committed
Document email template and system config variable endpoints
Part 2 of 3 for DSpace/dspace-angular#5828: REST contract for /api/system/emailtemplates (ETag concurrency, 304/412/428, 400 validation) and /api/system/systemconfigvariables (allowed config discovery, decoupled from template payloads).
1 parent 16bb43e commit 6356363

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

PULL_REQUEST.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## References
2+
3+
* Related to [DSpace/dspace-angular#5828](https://github.com/DSpace/dspace-angular/issues/5828)
4+
* Backend implementation: [DSpace/DSpace](https://github.com/DSpace/DSpace) (Branch: `feature/admin-email-templates`)
5+
* **Coordinated 3-Repository Feature (Part 2 of 3)**:
6+
* **Part 1 (Backend REST API & Services)**: `DSpace/DSpace`
7+
* **Part 2 (This PR - REST Contract Documentation)**: `DSpace/RestContract`
8+
* **Part 3 (Coming soon - Angular Admin UI & Management)**: `DSpace/dspace-angular` (Branch: `feature/admin-email-templates`)
9+
10+
---
11+
12+
## Description
13+
14+
Documents the two new admin-only endpoints for email template management:
15+
16+
* `/api/system/emailtemplates` (list, retrieve with `ETag`/`If-None-Match` → 304, update with `If-Match` → 428/412, wildcard `*` force-overwrite, 400 on VTL/XSS/length violations).
17+
* `/api/system/systemconfigvariables` (list + retrieve single allowed config variable: `key`, `value`, `placeholder`).
18+
19+
Key contract decision: configuration variables are **not** embedded in template payloads — clients fetch them once from the dedicated endpoint instead of receiving the identical list duplicated in every template.
20+
21+
---
22+
23+
## List of Changes in this PR
24+
25+
* `emailtemplates.md`: fixed stale draft — removed `configVariables`/`allowedConfigs` from the example response and properties (decoupled per backend), documented field constraints (name pattern, content 10–50000, subject max 500, variable description max 500, placeholder max 30), kept ETag concurrency section.
26+
* `systemconfigvariables.md` (new): list + retrieve-single contract with example payloads, field constraints (key pattern, value max 2000, placeholder max 255), and status codes (200/401/403/404).
27+
* `endpoints.md`: linked both pages.
28+
29+
---
30+
31+
## Checklist
32+
33+
* [x] My PR is created against the `main` branch of code.
34+
* [x] My documentation matches the backend implementation (`feature/admin-email-templates`: endpoint paths, `systemconfigvariable` type name, status codes, validation limits).

emailtemplates.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Email Templates
2+
3+
[Back to the list of all defined endpoints](endpoints.md)
4+
5+
This endpoint exposes DSpace email templates to administrators, allowing them to view and edit email templates stored in `{dspace.dir}/config/emails/`.
6+
7+
Only administrators are allowed to access and edit these templates.
8+
Templates cannot be created or deleted via the REST API because they correspond to hardcoded templates in the DSpace backend.
9+
10+
System configuration variables usable inside templates (e.g. `${config.get('dspace.name')}`) are deliberately **not** embedded in template payloads. They are exposed via the dedicated [/api/system/systemconfigvariables](systemconfigvariables.md) endpoint.
11+
12+
## Optimistic Concurrency Control
13+
14+
To prevent concurrent administrators from overwriting each other's changes, this endpoint implements optimistic concurrency control via HTTP conditional headers:
15+
16+
* **ETag Header**: Every `GET` and successful `PUT` response includes an `ETag` response header containing a SHA-256 hash of the template content (e.g., `ETag: "6a8f1b2c3d..."`).
17+
* **If-None-Match**: When retrieving a template with `GET`, clients may provide the `If-None-Match` header. If the template has not changed, the server responds with **304 Not Modified**.
18+
* **If-Match Required on PUT**: Every `PUT` request **must** provide the `If-Match` request header matching the current ETag (or `*` for unconditional overwrite).
19+
* If `If-Match` is missing, the server responds with **428 Precondition Required**.
20+
* If `If-Match` does not match the current template version, the server responds with **412 Precondition Failed**.
21+
22+
---
23+
24+
## Retrieve all email templates
25+
26+
GET `/api/system/emailtemplates`
27+
28+
Returns a list of all email templates, ordered alphabetically by name.
29+
30+
### Status Codes for Retrieve All
31+
32+
* 200 OK - if the operation succeeded
33+
* 401 Unauthorized - if not logged in
34+
* 403 Forbidden - if logged in as a non-admin user
35+
36+
---
37+
38+
## Retrieve a specific email template
39+
40+
GET `/api/system/emailtemplates/<:template-name>`
41+
42+
Returns a single email template by its filename identifier (e.g., `register`, `feedback`).
43+
44+
### Example Response
45+
46+
```json
47+
{
48+
"name": "register",
49+
"content": "## E-mail sent to DSpace users when they register for an account\n##\n## Parameters: {0} is expanded to a special registration URL\n##\n#set($subject = \"${config.get('dspace.name')} Account Registration\")\nTo complete registration, click: ${params[0]}\n",
50+
"subject": "${config.get('dspace.name')} Account Registration",
51+
"mimetype": "text/plain",
52+
"variables": [
53+
{
54+
"index": 0,
55+
"description": "is expanded to a special registration URL",
56+
"placeholder": "${params[0]}"
57+
}
58+
],
59+
"lastModified": "2026-09-05T12:00:00Z",
60+
"etag": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
61+
"type": "emailtemplate",
62+
"_links": {
63+
"self": {
64+
"href": "http://localhost:8080/server/api/system/emailtemplates/register"
65+
}
66+
}
67+
}
68+
```
69+
70+
### Template Properties
71+
72+
* **name**: Unique template identifier matching filename. Pattern `^[a-zA-Z0-9._-]+$`, max 255 chars.
73+
* **subject**: Extracted subject line from `#set($subject = ...)` directive, max 500 chars.
74+
* **mimetype**: Enum indicating the content type. Possible values:
75+
* `"text/plain"`: Standard plain text email (default).
76+
* `"text/html"`: HTML-formatted email, detected when the template specifies `#set($mimetype = "text/html")` (supported via [DSpace PR #11755](https://github.com/DSpace/DSpace/pull/11755)).
77+
* **content**: Raw VTL template body, 10 to 50,000 chars.
78+
* **variables**: Array of parsed parameter variables (`index`, `description` max 500 chars, `placeholder` max 30 chars e.g. `${params[0]}`).
79+
* **lastModified**: ISO-8601 timestamp of when the file was last modified on disk.
80+
* **etag**: Deterministic SHA-256 hash of the template content for concurrency control.
81+
82+
### Status Codes for Retrieve Specific
83+
84+
* 200 OK - if the operation succeeded
85+
* 304 Not Modified - if `If-None-Match` matches current ETag
86+
* 401 Unauthorized - if not logged in
87+
* 403 Forbidden - if logged in as a non-admin user
88+
* 404 Not Found - if no template exists with the specified name
89+
90+
---
91+
92+
## Update an email template
93+
94+
PUT `/api/system/emailtemplates/<:template-name>`
95+
96+
Replaces the full content of the email template. The template content is validated against Velocity (VTL) syntax rules, XSS sanitization (OWASP), and size constraints (10 to 50,000 characters).
97+
98+
### Required Request Headers
99+
100+
* `Content-Type: application/json`
101+
* `If-Match: "<current-etag>"` (or `*`)
102+
103+
### Example Request Body
104+
105+
```json
106+
{
107+
"content": "## E-mail sent to DSpace users when they register for an account\n##\n## Parameters: {0} is expanded to a special registration URL\n##\n#set($subject = \"${config.get('dspace.name')} Account Registration\")\nTo complete registration, please visit: ${params[0]}\n"
108+
}
109+
```
110+
111+
### Status Codes for Update
112+
113+
* 200 OK - if the template was successfully updated (returns updated template and new `ETag` header)
114+
* 400 Bad Request - if the content fails validation (invalid VTL syntax, XSS script tags, or length < 10 or > 50,000 chars)
115+
* 401 Unauthorized - if not logged in
116+
* 403 Forbidden - if logged in as a non-admin user
117+
* 404 Not Found - if no template exists with the specified name
118+
* 412 Precondition Failed - if the `If-Match` ETag is stale or does not match
119+
* 428 Precondition Required - if the `If-Match` header was omitted

endpoints.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
* [/api/submission/vocabularyEntryDetails](vocabularyEntryDetails.md)
4242
* [/api/system/auditevents](auditevents.md)
4343
* [/api/system/systemwidealerts](systemwidealerts.md)
44+
* [/api/system/emailtemplates](emailtemplates.md)
45+
* [/api/system/systemconfigvariables](systemconfigvariables.md)
4446
* [/api/versioning/versions](versions.md)
4547
* [/api/versioning/versionhistories](versionhistories.md)
4648
* [/api/workflow/workflowitems](workflowitems.md)

systemconfigvariables.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# System Config Variables
2+
3+
[Back to the list of all defined endpoints](endpoints.md)
4+
5+
This endpoint exposes the general repository configuration variables that administrators may reference inside email templates (e.g. `${config.get('dspace.name')}`).
6+
7+
It returns the subset of configuration properties allowed in templates, as defined by `message.templates.allowed-config` (resolved via `Utils.getAllowedTemplateConfig()`, matching `Email.java` and `LDN.java`).
8+
9+
Only administrators are allowed to access these variables. They are exposed here — decoupled from the [/api/system/emailtemplates](emailtemplates.md) payloads — so clients fetch the identical list once instead of receiving it duplicated inside every template.
10+
11+
---
12+
13+
## Retrieve all system config variables
14+
15+
GET `/api/system/systemconfigvariables`
16+
17+
Returns a list of all allowed system configuration variables, ordered alphabetically by key.
18+
19+
### Example Response (paginated, HAL-embedded)
20+
21+
```json
22+
{
23+
"_embedded": {
24+
"systemconfigvariables": [
25+
{
26+
"key": "dspace.name",
27+
"value": "DSpace at My University",
28+
"placeholder": "${config.get('dspace.name')}",
29+
"type": "systemconfigvariable"
30+
},
31+
{
32+
"key": "dspace.ui.url",
33+
"value": "http://localhost:4000",
34+
"placeholder": "${config.get('dspace.ui.url')}",
35+
"type": "systemconfigvariable"
36+
}
37+
]
38+
}
39+
}
40+
```
41+
42+
### Variable Properties
43+
44+
* **key**: Configuration property key (e.g. `dspace.name`). Pattern `^[a-zA-Z0-9._-]+$`, max 255 chars.
45+
* **value**: Resolved value of the configuration property, max 2000 chars.
46+
* **placeholder**: Velocity expression to insert into a template (e.g. `${config.get('dspace.name')}`), max 255 chars.
47+
48+
### Status Codes for Retrieve All
49+
50+
* 200 OK - if the operation succeeded
51+
* 401 Unauthorized - if not logged in
52+
* 403 Forbidden - if logged in as a non-admin user
53+
54+
---
55+
56+
## Retrieve a specific system config variable
57+
58+
GET `/api/system/systemconfigvariables/<:property-key>`
59+
60+
Returns a single configuration variable by its property key (e.g., `dspace.name`).
61+
62+
### Example Response
63+
64+
```json
65+
{
66+
"key": "dspace.name",
67+
"value": "DSpace at My University",
68+
"placeholder": "${config.get('dspace.name')}",
69+
"type": "systemconfigvariable",
70+
"_links": {
71+
"self": {
72+
"href": "http://localhost:8080/server/api/system/systemconfigvariables/dspace.name"
73+
}
74+
}
75+
}
76+
```
77+
78+
### Status Codes for Retrieve Specific
79+
80+
* 200 OK - if the operation succeeded
81+
* 401 Unauthorized - if not logged in
82+
* 403 Forbidden - if logged in as a non-admin user
83+
* 404 Not Found - if no allowed variable exists with the specified key

0 commit comments

Comments
 (0)