Skip to content

Commit caf3667

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 caf3667

3 files changed

Lines changed: 225 additions & 0 deletions

File tree

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: "da56428b0c..."`).
17+
* **If-None-Match**: When retrieving a template with `GET`, clients may provide the `If-None-Match` header containing an entity tag, a comma-separated list of entity tags, weak validators (e.g. `W/"..."`), or `*`. If any candidate matches the current template, the server responds with **304 Not Modified**.
18+
* **If-Match Required on PUT**: Every `PUT` request **must** provide the `If-Match` request header. It accepts an entity tag, a comma-separated list of entity tags, weak validators (e.g. `W/"..."`), or `*` for unconditional overwrite.
19+
* If `If-Match` is missing, the server responds with **428 Precondition Required**.
20+
* If none of the provided ETags 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": "da56428b0c502de5a058cdd47cdf5fc446d90fc68ed2cf096d3bf6ae53ba568a",
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 any candidate in `If-None-Match` matches current ETag (or `*`)
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 submitted content is preserved as-is (retaining leading indentation and blank lines) while ensuring a trailing newline. 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>"` (also accepts a comma-separated list of ETags, `W/"<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 none of the candidate ETags in `If-Match` match the current template version
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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 repository configuration (e.g. `message.templates.allowed-config`).
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+
"_links": {
31+
"self": {
32+
"href": "http://localhost:8080/server/api/system/systemconfigvariables/dspace.name"
33+
}
34+
}
35+
},
36+
{
37+
"key": "dspace.ui.url",
38+
"value": "http://localhost:4000",
39+
"placeholder": "${config.get('dspace.ui.url')}",
40+
"type": "systemconfigvariable",
41+
"_links": {
42+
"self": {
43+
"href": "http://localhost:8080/server/api/system/systemconfigvariables/dspace.ui.url"
44+
}
45+
}
46+
}
47+
]
48+
},
49+
"_links": {
50+
"self": {
51+
"href": "http://localhost:8080/server/api/system/systemconfigvariables"
52+
}
53+
},
54+
"page": {
55+
"size": 20,
56+
"totalElements": 2,
57+
"totalPages": 1,
58+
"number": 0
59+
}
60+
}
61+
```
62+
63+
### Variable Properties
64+
65+
* **key**: Configuration property key (e.g. `dspace.name`). Pattern `^[a-zA-Z0-9._-]+$`, max 255 chars.
66+
* **value**: Resolved value of the configuration property, max 2000 chars.
67+
* **placeholder**: Velocity expression to insert into a template (e.g. `${config.get('dspace.name')}`), max 255 chars.
68+
69+
### Status Codes for Retrieve All
70+
71+
* 200 OK - if the operation succeeded
72+
* 401 Unauthorized - if not logged in
73+
* 403 Forbidden - if logged in as a non-admin user
74+
75+
---
76+
77+
## Retrieve a specific system config variable
78+
79+
GET `/api/system/systemconfigvariables/<:property-key>`
80+
81+
Returns a single configuration variable by its property key (e.g., `dspace.name`).
82+
83+
### Example Response
84+
85+
```json
86+
{
87+
"key": "dspace.name",
88+
"value": "DSpace at My University",
89+
"placeholder": "${config.get('dspace.name')}",
90+
"type": "systemconfigvariable",
91+
"_links": {
92+
"self": {
93+
"href": "http://localhost:8080/server/api/system/systemconfigvariables/dspace.name"
94+
}
95+
}
96+
}
97+
```
98+
99+
### Status Codes for Retrieve Specific
100+
101+
* 200 OK - if the operation succeeded
102+
* 401 Unauthorized - if not logged in
103+
* 403 Forbidden - if logged in as a non-admin user
104+
* 404 Not Found - if no allowed variable exists with the specified key

0 commit comments

Comments
 (0)