Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import com.condation.cms.modules.ui.http.auth.CSRFHandler;
import com.condation.cms.modules.ui.http.auth.LoginResourceHandler;
import com.condation.cms.modules.ui.http.auth.LogoutHandler;
import com.condation.cms.modules.ui.http.auth.RefreshTokenHandler;
import com.condation.cms.modules.ui.http.auth.UIAuthHandler;
import com.condation.cms.modules.ui.http.auth.UIAuthRedirectHandler;
import com.condation.cms.modules.ui.services.RemoteMethodService;
Expand Down Expand Up @@ -135,6 +136,7 @@ public Mapping getMapping() {

try {

mapping.add(PathSpec.from("/manager/refresh"), new RefreshTokenHandler(getContext(), getRequestContext()));
mapping.add(PathSpec.from("/manager/login"), new LoginResourceHandler(getContext(), getRequestContext()));
//mapping.add(PathSpec.from("/manager/login.action"), new LoginHandler(getContext(), getRequestContext(), failedLoginsCounter));
mapping.add(PathSpec.from("/manager/login.action"), new AjaxLoginHandler(getContext(), getRequestContext(), failedLoginsCounter, logins));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.condation.cms.modules.ui.http.auth;

/*-
* #%L
* UI Module
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import com.condation.cms.api.configuration.configs.ServerConfiguration;
import com.condation.cms.api.feature.features.ConfigurationFeature;
import com.condation.cms.api.module.SiteModuleContext;
import com.condation.cms.api.request.RequestContext;
import com.condation.cms.modules.ui.http.JettyHandler;
import com.condation.cms.modules.ui.utils.AuthUtil;
import com.condation.cms.modules.ui.utils.CookieUtil;
import com.condation.cms.modules.ui.utils.TokenUtils;
import com.condation.cms.modules.ui.utils.UIConstants;
import com.condation.cms.modules.ui.utils.json.UIGsonProvider;
import java.time.Duration;
import java.util.Map;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* Handler für den expliziten Refresh-Endpunkt.
*
* Die Route /manager/refresh verwendet hier das Refresh-Cookie, um bei Bedarf
* neue Auth- und Refresh-Tokens zu setzen.
*/
@RequiredArgsConstructor
@Slf4j
public class RefreshTokenHandler extends JettyHandler {

private final SiteModuleContext moduleContext;
private final RequestContext requestContext;

@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
if (!request.getMethod().equalsIgnoreCase("POST")) {
return false;
}

boolean refreshed = AuthUtil.refreshTokens(request, response, moduleContext, requestContext);
if (refreshed) {
var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret();

var authCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_TOKEN);

var token = authCookie.get().getValue();

var payload = TokenUtils.getPayload(token, secret);

response.setStatus(200);
Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of(
"status", "ok",
"previewToken", TokenUtils.createToken(payload.get().username(), secret, Duration.ofHours(1), Duration.ofDays(7))
)), callback);
} else {
response.setStatus(401);
Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of("status", "error", "reason", "unauthorized")), callback);
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,21 @@ private static boolean tryRefresh(Request request, Response response, SiteModule

var payload = TokenUtils.getPayload(token, secret);
if (payload.isPresent()) {
if (refreshTokenCache.contains(token)) {
refreshTokenCache.invalidate(token);
refreshTokenCache.invalidate(token); // best-effort invalidation; refresh should still work after restart if token is valid

Optional<User> userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), payload.get().username());
if (userOpt.isPresent()) {
updateCookies(userOpt.get(), response, requestContext, moduleContext);
return true;
}
Optional<User> userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), payload.get().username());
if (userOpt.isPresent()) {
updateCookies(userOpt.get(), response, requestContext, moduleContext);
return true;
}
}
return false;
}

public static boolean refreshTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) {
return tryRefresh(request, response, moduleContext, requestContext);
}

public static boolean checkAuthTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) {
var authCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_TOKEN);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export async function runAction(params) {
const wrapper = document.getElementById("cmsFocalWrapper");
const image = document.getElementById("cms-image");
const point = document.getElementById("cmsFocalPoint");
if (wrapper === null || image === null || point === null) {
console.error("One or more required elements not found");
return;
}
if (image.complete) {
setFocalPoint(image, point, focalX, focalY);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function runAction(params) {
onOk: async (event) => {
},
onShow: async (modalElement) => {
modalElement.querySelectorAll('button[data-action]').forEach(button => {
modalElement.querySelectorAll('button[data-action]').forEach((button) => {
button.addEventListener('click', async (e) => {
const action = e.currentTarget.getAttribute('data-action');
const siteId = e.currentTarget.getAttribute('data-id');
Expand Down
3 changes: 2 additions & 1 deletion modules/ui-module/src/main/resources/manager/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
baseUrl: '{{ managerBaseURL }}',
contextPath: '{{ contextPath }}',
siteId: '{{ siteId }}',
previewUrl: "{{ links.createUrl('/?preview=manager') | raw }}"
previewUrl: "{{ links.createUrl('/?preview=manager') | raw }}",
refreshUrl: "{{ links.createUrl('/manager/refresh') | raw }}"
}
</script>
<link rel="stylesheet" href="{{ links.createUrl('/manager/css/manager.css') }}" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use strict";
/*-
* #%L
* UI Module
Expand Down
13 changes: 13 additions & 0 deletions modules/ui-module/src/main/resources/manager/js/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,20 @@ import { setCSRFToken } from '@cms/modules/utils.js';
frameMessenger.on('load', (payload) => {
EventBus.emit("preview:loaded", {});
});
function heartbeat() {
fetch(window.manager.refreshUrl, {
method: "POST",
credentials: "include"
})
.then(res => res.json())
.then(data => {
window.manager.previewToken = data.previewToken;
});
}
document.addEventListener("DOMContentLoaded", function () {
setInterval(() => {
heartbeat();
}, 10 * 1000);
//PreviewHistory.init("/");
//updateStateButton();
activatePreviewOverlay();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
*/
export function openFileBrowser(optionsParam: any): Promise<void>;
export namespace state {
let options: any;
let options: null;
let currentFolder: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const createCheckboxField = (options, value = []) => {
const key = options.key || "";
const name = options.name || id;
const title = options.title || "";
const choices = options.options.choices || [];
const choices = options.options?.choices || [];
const selectedValues = new Set(value);
const checkboxes = choices.map((choice, idx) => {
const inputId = `${id}-${idx}`;
Expand All @@ -46,7 +46,10 @@ const createCheckboxField = (options, value = []) => {
`;
};
const getData = (context) => {
const data = {};
var data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='checkbox']").forEach(container => {
const name = container.querySelector("input[type='checkbox']").name;
const checkedBoxes = container.querySelectorAll("input[type='checkbox']:checked");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const createColorField = (options, value = '#000000') => {
};
const getColorData = (context) => {
const data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='color'] input").forEach((el) => {
data[el.name] = {
type: 'color',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const createDateField = (options, value = '') => {
};
const getDateData = (context) => {
const data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='date'] input").forEach((el) => {
const value = getUTCDateFromInput(el); // "2025-05-31"
data[el.name] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const createDateTimeField = (options, value = '') => {
};
const getDateTimeData = (context) => {
const data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='datetime'] input").forEach((el) => {
const value = getUTCDateTimeFromInput(el); // "2025-05-31T15:30"
data[el.name] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const createMarkdownField = (options, value = '') => {
};
const getData = (context) => {
const data = {};
if (!context.formElement) {
return data;
}
markdownEditors.forEach(({ input, editor }) => {
data[input.name] = {
type: "easymde",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const handleAddItem = (e, container, context) => {
</div>
`;
listGroup.insertAdjacentHTML("beforeend", itemMarkup);
var itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`);
const itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`);
if (itemElement) {
itemElement.addEventListener('dblclick', (e) => handleDoubleClick(e, context));
const removeBtn = itemElement.querySelector('.remove-btn');
Expand All @@ -99,7 +99,7 @@ const getItemForm = async (el) => {
const getContentResponse = await getContent({
uri: contentNode.result.uri
});
var selected = pageTemplates.filter(pageTemplate => pageTemplate.template === getContentResponse?.result?.meta?.template);
var selected = pageTemplates.filter((pageTemplate) => pageTemplate.template === getContentResponse?.result?.meta?.template);
const listContainer = el.closest("[data-cms-form-field-type='list']");
const fieldName = listContainer?.getAttribute('name');
var itemForm = [];
Expand All @@ -108,7 +108,7 @@ const getItemForm = async (el) => {
}
if (!itemForm || itemForm.length === 0) {
let itemTypes = (await getListItemTypes({})).result;
var selectedItemType = itemTypes.filter(itemType => itemType.name === fieldName);
var selectedItemType = itemTypes.filter((itemType) => itemType.name === fieldName);
itemForm = (selectedItemType.length === 1) ? selectedItemType[0].data?.form.fields : [];
}
return itemForm;
Expand Down Expand Up @@ -136,16 +136,22 @@ const handleDoubleClick = async (event, context) => {
el.setAttribute('data-cms-form-field-item-data', JSON.stringify(updateData));
const listContainer = el.closest("[data-cms-form-field-type='list']");
const nameField = listContainer?.getAttribute('data-name-field') || 'name';
el.querySelector('.object-name').textContent = updateData[nameField];
const objectNameEl = el.querySelector('.object-name');
if (!objectNameEl)
return;
objectNameEl.textContent = updateData[nameField] || "";
}
});
}
};
const getData = (context) => {
var data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach((el) => {
let value = [];
el.querySelectorAll("[data-cms-form-field-item]").forEach(itemEl => {
el.querySelectorAll("[data-cms-form-field-item]").forEach((itemEl) => {
const itemData = itemEl.getAttribute('data-cms-form-field-item-data');
if (itemData) {
value.push(JSON.parse(itemData));
Expand All @@ -162,7 +168,7 @@ const getData = (context) => {
return data;
};
const init = (context) => {
context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => {
context.formElement?.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => {
listContainer.querySelectorAll("[data-cms-form-field-item]").forEach(field => {
field.addEventListener('dblclick', (e) => handleDoubleClick(e, context));
// Remove-Button-Listener setzen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const createEmailField = (options, value = '') => {
};
const getData = (context) => {
var data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='mail'] input").forEach((el) => {
let value = el.value;
data[el.name] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ const createMarkdownField = (options, value = '') => {
};
const getData = (context) => {
const data = {};
const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input');
const formElement = context.formElement;
if (!formElement) {
return data;
}
const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input');
editorInputs.forEach((input) => {
const editor = input.cherryEditor;
if (editor && editor.getMarkdown) {
Expand All @@ -58,8 +62,12 @@ const getData = (context) => {
return data;
};
const init = async (context) => {
const formElement = context.formElement;
if (!formElement) {
return;
}
const cmsTagsMenu = await buildCmsTagsMenu();
const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input');
const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input');
editorInputs.forEach((input) => {
const containerId = input.dataset.cherryId;
const initialValue = decodeURIComponent(input.dataset.initialValue || "");
Expand Down Expand Up @@ -113,7 +121,7 @@ const getEditorFromEvent = (event) => {
const buildCmsTagsMenu = async () => {
const response = await getTagNames({});
const tagNames = response.result || [];
const submenuConfig = tagNames.map(tag => ({
const submenuConfig = tagNames.map((tag) => ({
name: tag.charAt(0).toUpperCase() + tag.slice(1),
value: tag,
noIcon: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const createMediaField = (options, value = '') => {
};
const getData = (context) => {
const data = {};
if (!context.formElement) {
return data;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => {
const input = wrapper.querySelector(".cms-media-input-value");
if (input) {
Expand All @@ -68,6 +71,9 @@ const getData = (context) => {
return data;
};
const init = (context) => {
if (!context.formElement) {
return;
}
context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => {
const dropZone = wrapper.querySelector(".cms-drop-zone");
const input = wrapper.querySelector(".cms-media-input");
Expand Down Expand Up @@ -101,7 +107,14 @@ const init = (context) => {
//dropZone.addEventListener("click", () => input.click());
// Handle file selection
input.addEventListener("change", (e) => {
const file = e.target.files[0];
if (e.target === null) {
return;
}
var inputElement = e.target;
if (inputElement.files == null) {
return;
}
const file = inputElement.files[0];
if (file) {
preview.src = URL.createObjectURL(file);
handleUpload(wrapper, file);
Expand Down
Loading