Skip to content

Commit 514d26f

Browse files
committed
Teedy v2.6.0: unified navigation, security hardening, file drop zones, color palette
1 parent 0ea73c2 commit 514d26f

75 files changed

Lines changed: 3291 additions & 1911 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-deploy.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,21 @@ jobs:
7373
path: docs-web/target
7474

7575
- name: Set up QEMU
76-
uses: docker/setup-qemu-action@v3
76+
uses: docker/setup-qemu-action@v4
7777

7878
- name: Set up Docker Buildx
79-
uses: docker/setup-buildx-action@v3
79+
uses: docker/setup-buildx-action@v4
8080

8181
- name: Log in to GHCR
82-
uses: docker/login-action@v3
82+
uses: docker/login-action@v4
8383
with:
8484
registry: ${{ env.REGISTRY }}
8585
username: ${{ github.actor }}
8686
password: ${{ secrets.GITHUB_TOKEN }}
8787

8888
- name: Docker metadata
8989
id: metadata
90-
uses: docker/metadata-action@v5
90+
uses: docker/metadata-action@v6
9191
with:
9292
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
9393
flavor: |
@@ -98,7 +98,7 @@ jobs:
9898
type=raw,value={{branch}}-rc-{{sha}},enable=${{ startsWith(github.ref, 'refs/heads/release/') }}
9999
100100
- name: Build and push
101-
uses: docker/build-push-action@v5
101+
uses: docker/build-push-action@v7
102102
with:
103103
context: .
104104
platforms: linux/amd64,linux/arm64

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,4 @@ USER jetty
7373
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
7474
CMD curl -f http://localhost:8080/api/user || exit 1
7575

76-
CMD java ${JAVA_OPTIONS} -jar /opt/jetty/start.jar
76+
CMD ["sh", "-c", "exec java ${JAVA_OPTIONS} -jar /opt/jetty/start.jar"]

ROADMAP.md

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -139,50 +139,79 @@ See [release notes](https://github.com/fmaass/teedy-docs/releases/tag/v2.5.0) fo
139139

140140
---
141141

142-
## v2.6.0 (Planned)
142+
## v2.6.0 (Released)
143143

144144
**Theme:** Security Hardening + Unified Navigation
145145

146146
### Unified document view
147147

148-
- Merge Documents and Browse into a single view with three zones: tag tree sidebar (left), address bar with active/related tags and search (top), document list (main)
149-
- Tag tree always visible alongside documents, with facet-driven counts and auto-expand to active branches
150-
- Document slide-over panel (side peek) to preview documents without leaving the list
148+
- Merged Documents and Browse into a single three-zone layout: tag tree sidebar (left), search bar with filter chips (top), document list (main)
149+
- Tag tree always visible with facet-driven counts and auto-expand to active branches
150+
- Document slide-over panel to preview documents without leaving the list
151151
- AND/OR toggle for tag intersection vs union mode
152-
- Tag exclusion UI: tri-state per tag (neutral / included / excluded) with visual differentiation on chips; backend `!tag:` / `search[nottag]` already supports this
153-
- Untagged document filter: "Untagged" pseudo-node or toggle to surface documents with zero tags
154-
- Quick tagging from document list: right-click context menu with tag picker to add/remove tags without opening the edit form
155-
- Replaces the separate Documents and Browse navigation items with a single "Documents" view
152+
- Tag exclusion UI: tri-state per tag (neutral / included / excluded) with visual chips
153+
- Quick tagging from document list context menu
156154

157-
### Login brute force protection
155+
### File drop zones
158156

159-
- Per-IP and per-username rate limiting with exponential backoff after failed attempts
160-
- Configurable thresholds via env vars
157+
- Drag-and-drop file upload on document edit form and Files tab
158+
- Visual feedback on drag hover, pending file list with sizes
159+
- Direct upload on Files tab without entering edit mode
161160

162-
### Password change verification
161+
### Auto-tag from filter
163162

164-
- Require current password when changing password via self-update endpoint
165-
- Frontend form update
163+
- New documents pre-populate tags from currently selected tags in the tag tree
164+
- Convenience default — users can remove tags before saving
166165

167-
### File upload size limits
166+
### Security hardening
168167

169-
- Configurable maximum upload size (env var, default 500MB)
170-
- Enforce at stream level before writing to disk
168+
- Login brute force protection: per-IP and per-username rate limiting with exponential backoff, HTTP 429 + Retry-After header, 15-minute max lockout
169+
- Session token lifetime reduced from 20 years to 90 days with sliding expiry (token rotation on authenticated requests)
170+
- Password complexity enforcement: minimum 8 characters, mixed case + digit, reject username as password
171+
- Auth cookie Secure + HttpOnly flags, security response headers
172+
- Lucene: removed NoLockFactory, commit-only-on-success, synchronized reader access
171173

172-
### OIDC linking security
174+
### Upload size limits
173175

174-
- Prevent auto-linking OIDC accounts to existing local accounts without explicit authorization
175-
- Require local login or admin approval for first-time binding
176+
- Configurable maximum upload size via `DOCS_MAX_UPLOAD_SIZE` env var (default 500 MB)
177+
- Exposed in Settings UI as read-only system info
176178

177-
### Session token lifetime
179+
### Unified color palette
178180

179-
- Reduce "remember me" token lifetime from 20 years to 90 days
180-
- Token rotation on authenticated requests
181+
- Self-contained primary color ramp derived from Teedy blue (#2aabd2), no external palette references
182+
- Status colors (success/warning/danger/info) use PrimeVue semantic tokens for automatic dark mode and theme switching
183+
- Design token system via teedy-tokens.css with PrimeVue variable delegation
181184

182-
### Password complexity
185+
### Frontend modernization
183186

184-
- Enforce mixed-case + digit or zxcvbn-based strength check
185-
- Reject passwords matching the username
187+
- Component decomposition: AppHeader, TagTreePanel, TagFilterChips, DocumentSearchBar, DocumentTable, DocumentSlideOver, PdfViewer
188+
- PDF.js canvas renderer replacing iframe embeds
189+
- Accessibility: ARIA labels on icon-only buttons, ARIA tab roles, PrimeVue Select components
190+
- Design tokens and PrimeVue migration across settings, document, and tag views
191+
192+
### Infrastructure
193+
194+
- Docker CMD JSON form for proper signal forwarding
195+
- GitHub Actions upgraded to Node.js 24 compatible versions
196+
- Legacy AngularJS removed (180 dead files from src-legacy/)
197+
198+
See [release notes](https://github.com/fmaass/teedy-docs/releases/tag/v2.6.0) for details.
199+
200+
---
201+
202+
## v2.7.0 (Planned)
203+
204+
**Theme:** TBD
205+
206+
### Password change verification
207+
208+
- Require current password when changing password via self-update endpoint
209+
- Frontend form update
210+
211+
### OIDC linking security
212+
213+
- Prevent auto-linking OIDC accounts to existing local accounts without explicit authorization
214+
- Require local login or admin approval for first-time binding
186215

187216
### Bulk operations
188217

docs-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>com.sismics.docs</groupId>
77
<artifactId>docs-parent</artifactId>
8-
<version>2.5.0</version>
8+
<version>2.6.0</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111

docs-core/src/main/java/com/sismics/docs/core/dao/AuthenticationTokenDao.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ public void updateLastConnectionDate(String id) {
9797
q.executeUpdate();
9898
}
9999

100+
/**
101+
* Updates the creation date of a token (used for token rotation / sliding expiry).
102+
*
103+
* @param authenticationToken Authentication token with updated creation date
104+
*/
105+
public void updateCreationDate(AuthenticationToken authenticationToken) {
106+
StringBuilder sb = new StringBuilder("update T_AUTHENTICATION_TOKEN ato ");
107+
sb.append(" set AUT_CREATEDATE_D = :creationDate ");
108+
sb.append(" where ato.AUT_ID_C = :id");
109+
110+
EntityManager em = ThreadLocalContext.get().getEntityManager();
111+
Query q = em.createNativeQuery(sb.toString());
112+
q.setParameter("creationDate", authenticationToken.getCreationDate());
113+
q.setParameter("id", authenticationToken.getId());
114+
q.executeUpdate();
115+
}
116+
100117
/**
101118
* Returns all authentication tokens of an user.
102119
*

docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,5 +306,58 @@ public long countDocumentsWithAllTags(List<String> tagIds) {
306306
q.setParameter("tagCount", (long) tagIds.size());
307307
return ((Number) q.getSingleResult()).longValue();
308308
}
309+
310+
/**
311+
* Returns tags that co-occur with any of the given selected tags (OR logic).
312+
*
313+
* @param selectedTagIds List of currently selected tag IDs
314+
* @return Map of tag ID to document count (excludes already-selected tags)
315+
*/
316+
@SuppressWarnings("unchecked")
317+
public Map<String, Long> getCoOccurringTagCountsOr(List<String> selectedTagIds) {
318+
if (selectedTagIds == null || selectedTagIds.isEmpty()) {
319+
return getTagDocumentCounts();
320+
}
321+
322+
EntityManager em = ThreadLocalContext.get().getEntityManager();
323+
Query q = em.createNativeQuery(
324+
"select dt.DOT_IDTAG_C, count(distinct dt.DOT_IDDOCUMENT_C) " +
325+
"from T_DOCUMENT_TAG dt " +
326+
"join T_DOCUMENT d on d.DOC_ID_C = dt.DOT_IDDOCUMENT_C and d.DOC_DELETEDATE_D is null " +
327+
"where dt.DOT_DELETEDATE_D is null " +
328+
"and dt.DOT_IDTAG_C not in (:selectedTagIds) " +
329+
"and dt.DOT_IDDOCUMENT_C in (" +
330+
" select dt2.DOT_IDDOCUMENT_C from T_DOCUMENT_TAG dt2 " +
331+
" where dt2.DOT_IDTAG_C in (:selectedTagIds) " +
332+
" and dt2.DOT_DELETEDATE_D is null " +
333+
") " +
334+
"group by dt.DOT_IDTAG_C");
335+
q.setParameter("selectedTagIds", selectedTagIds);
336+
List<Object[]> rows = q.getResultList();
337+
Map<String, Long> result = new HashMap<>();
338+
for (Object[] row : rows) {
339+
result.put((String) row[0], ((Number) row[1]).longValue());
340+
}
341+
return result;
342+
}
343+
344+
/**
345+
* Counts documents matching any of the given tags (OR logic).
346+
*
347+
* @param tagIds Tag IDs
348+
* @return Number of matching documents
349+
*/
350+
public long countDocumentsWithAnyTag(List<String> tagIds) {
351+
if (tagIds == null || tagIds.isEmpty()) {
352+
return 0;
353+
}
354+
EntityManager em = ThreadLocalContext.get().getEntityManager();
355+
Query q = em.createNativeQuery(
356+
"select count(distinct dt.DOT_IDDOCUMENT_C) from T_DOCUMENT_TAG dt " +
357+
"join T_DOCUMENT d on d.DOC_ID_C = dt.DOT_IDDOCUMENT_C and d.DOC_DELETEDATE_D is null " +
358+
"where dt.DOT_IDTAG_C in (:tagIds) and dt.DOT_DELETEDATE_D is null");
359+
q.setParameter("tagIds", tagIds);
360+
return ((Number) q.getSingleResult()).longValue();
361+
}
309362
}
310363

docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ public class DocumentCriteria {
9393
*/
9494
private Boolean deleted;
9595

96+
/**
97+
* Tag combination mode: "and" (default) requires all tag groups to match,
98+
* "or" requires any tag group to match.
99+
*/
100+
private String tagMode = "and";
101+
96102
public List<String> getTargetIdList() {
97103
return targetIdList;
98104
}
@@ -208,4 +214,12 @@ public Boolean getDeleted() {
208214
public void setDeleted(Boolean deleted) {
209215
this.deleted = deleted;
210216
}
217+
218+
public String getTagMode() {
219+
return tagMode;
220+
}
221+
222+
public void setTagMode(String tagMode) {
223+
this.tagMode = tagMode;
224+
}
211225
}

docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ public void findByCriteria(PaginatedList<DocumentDto> paginatedList, List<String
312312
parameterMap.put("title", criteria.getTitleList());
313313
}
314314
if (!criteria.getTagIdList().isEmpty()) {
315+
boolean orMode = "or".equalsIgnoreCase(criteria.getTagMode());
315316
int index = 0;
317+
List<String> allTagCriteria = orMode ? Lists.newArrayList() : null;
316318
for (List<String> tagIdList : criteria.getTagIdList()) {
317319
List<String> tagCriteriaList = Lists.newArrayList();
318320
for (String tagId : tagIdList) {
@@ -321,7 +323,14 @@ public void findByCriteria(PaginatedList<DocumentDto> paginatedList, List<String
321323
tagCriteriaList.add(String.format("dt%d.DOT_ID_C is not null", index));
322324
index++;
323325
}
324-
criteriaList.add("(" + Joiner.on(" OR ").join(tagCriteriaList) + ")");
326+
if (orMode) {
327+
allTagCriteria.addAll(tagCriteriaList);
328+
} else {
329+
criteriaList.add("(" + Joiner.on(" OR ").join(tagCriteriaList) + ")");
330+
}
331+
}
332+
if (orMode && !allTagCriteria.isEmpty()) {
333+
criteriaList.add("(" + Joiner.on(" OR ").join(allTagCriteria) + ")");
325334
}
326335
}
327336
if (!criteria.getExcludedTagIdList().isEmpty()) {

docs-web-common/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>com.sismics.docs</groupId>
77
<artifactId>docs-parent</artifactId>
8-
<version>2.5.0</version>
8+
<version>2.6.0</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111

docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,29 @@ public static Date validateDate(String s, String name, boolean nullable) throws
221221
throw new ClientException("ValidationError", MessageFormat.format("{0} must be a date", name));
222222
}
223223
}
224+
225+
/**
226+
* Validates password strength: 8+ chars, at least one uppercase, one lowercase, one digit.
227+
* Rejects passwords matching the username (case-insensitive).
228+
*
229+
* @param password Password to validate
230+
* @param username Username to compare against
231+
*/
232+
public static void validatePasswordStrength(String password, String username) {
233+
if (password == null || password.length() < 8) {
234+
throw new ClientException("ValidationError", "Password must be at least 8 characters");
235+
}
236+
boolean hasUpper = false, hasLower = false, hasDigit = false;
237+
for (char c : password.toCharArray()) {
238+
if (Character.isUpperCase(c)) hasUpper = true;
239+
else if (Character.isLowerCase(c)) hasLower = true;
240+
else if (Character.isDigit(c)) hasDigit = true;
241+
}
242+
if (!hasUpper || !hasLower || !hasDigit) {
243+
throw new ClientException("ValidationError", "Password must contain at least one uppercase letter, one lowercase letter, and one digit");
244+
}
245+
if (username != null && password.equalsIgnoreCase(username)) {
246+
throw new ClientException("ValidationError", "Password must not match the username");
247+
}
248+
}
224249
}

0 commit comments

Comments
 (0)