Skip to content

Commit 00f3a58

Browse files
committed
feat: demo mode
1 parent 9c630c8 commit 00f3a58

File tree

10 files changed

+466
-21
lines changed

10 files changed

+466
-21
lines changed

app/controllers/onlyoffice_discourse/onlyoffice_controller.rb

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
class Onlyoffice::OnlyofficeController < ::ApplicationController
1212
include Onlyoffice::ControllerExtensions
1313

14-
requires_login except: [:callback, :formats, :editor]
15-
protect_from_forgery except: [:callback, :formats]
16-
skip_before_action :verify_authenticity_token, only: [:callback, :formats]
17-
skip_before_action :redirect_to_login_if_required, only: [:formats, :callback, :editor]
18-
skip_before_action :check_xhr, only: [:formats, :editor, :callback, :convert]
14+
requires_login except: [:callback, :formats, :editor, :demo_info]
15+
protect_from_forgery except: [:callback, :formats, :demo_info]
16+
skip_before_action :verify_authenticity_token, only: [:callback, :formats, :demo_info]
17+
skip_before_action :redirect_to_login_if_required, only: [:formats, :callback, :editor, :demo_info]
18+
skip_before_action :check_xhr, only: [:formats, :editor, :callback, :convert, :demo_info]
1919
skip_before_action :preload_json, only: [:callback]
2020
skip_before_action :redirect_to_profile_if_required, only: [:callback]
2121

@@ -29,6 +29,18 @@ def formats
2929
end
3030
end
3131

32+
def demo_info
33+
data = Onlyoffice::OnlyofficeDemo.demo_data
34+
expiration_date = Onlyoffice::OnlyofficeDemo.expiration_date
35+
36+
render json: {
37+
enabled: data[:enabled],
38+
available: data[:available],
39+
days_remaining: Onlyoffice::OnlyofficeDemo.days_remaining,
40+
expiration_date: expiration_date ? expiration_date.iso8601 : nil
41+
}
42+
end
43+
3244
def create
3345
document_type = params[:document_type] || "docx"
3446
locale = params[:locale] || "en"
@@ -145,6 +157,15 @@ def create
145157
def editor
146158
upload_id = params[:id].to_s.sub(/\.json$/, '')
147159

160+
# Check if demo mode is enabled but expired
161+
demo_data = Onlyoffice::OnlyofficeDemo.demo_data
162+
if demo_data[:enabled] && !demo_data[:available]
163+
return render json: {
164+
error: I18n.t("site_settings.onlyoffice_connector.demo.expired_error"),
165+
demo_expired: true
166+
}, status: :forbidden
167+
end
168+
148169
if request.format.json?
149170
upload = find_upload_by_short_url(upload_id)
150171

@@ -224,13 +245,25 @@ def editor
224245
doc_config[:token] = Onlyoffice::OnlyofficeJwt.generate_editor_token(doc_config)
225246
end
226247

227-
render json: {
248+
response_data = {
228249
config: {
229250
ds_host: SiteSetting.ONLYOFFICE_Docs_address,
230251
},
231252
id: params[:id],
232253
doc_config: doc_config
233254
}
255+
256+
# Add demo mode info if enabled
257+
if demo_data[:enabled] && demo_data[:available]
258+
expiration_date = Onlyoffice::OnlyofficeDemo.expiration_date
259+
response_data[:demo_mode] = {
260+
enabled: true,
261+
days_remaining: Onlyoffice::OnlyofficeDemo.days_remaining,
262+
expiration_date: expiration_date ? expiration_date.iso8601 : nil
263+
}
264+
end
265+
266+
render json: response_data
234267
else
235268
render "default/empty", formats: [:html]
236269
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Jobs
4+
class CheckDemoExpiration < ::Jobs::Scheduled
5+
every 1.day
6+
7+
def execute(args)
8+
return unless SiteSetting.onlyoffice_connector_enabled
9+
10+
data = Onlyoffice::OnlyofficeDemo.demo_data
11+
12+
if data[:enabled] && !data[:available]
13+
Rails.logger.info("ONLYOFFICE: Demo mode expired, disabling...")
14+
Onlyoffice::OnlyofficeDemo.enable_demo(false)
15+
SiteSetting.Connect_to_demo_ONLYOFFICE_Docs_server = false
16+
end
17+
end
18+
end
19+
end

assets/javascripts/discourse/components/modal/onlyoffice-editor-modal.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { action } from "@ember/object";
44
import { service } from "@ember/service";
55
import { ajax } from "discourse/lib/ajax";
66
import { popupAjaxError } from "discourse/lib/ajax-error";
7+
import I18n from "discourse-i18n";
78

89
export default class OnlyofficeEditorModal extends Component {
910
@service siteSettings;
11+
@service toasts;
1012

1113
@tracked isLoading = true;
1214
@tracked editorConfig = null;
@@ -35,7 +37,7 @@ async loadEditorConfig() {
3537
try {
3638
const uploadShortUrl = this.args.model.uploadShortUrl;
3739
const viewParam = this.args.model.viewOnly ? "?view=1" : "";
38-
40+
3941
const response = await ajax(
4042
`/onlyoffice/editor/${uploadShortUrl}.json${viewParam}`,
4143
{
@@ -48,6 +50,24 @@ async loadEditorConfig() {
4850
response.doc_config = JSON.parse(response.doc_config);
4951
}
5052

53+
// Show demo mode notification if enabled
54+
if (response.demo_mode?.enabled) {
55+
const expirationDate = response.demo_mode.expiration_date;
56+
const formattedDate = expirationDate
57+
? new Date(expirationDate).toLocaleDateString()
58+
: "";
59+
60+
const message = I18n.t("js.onlyoffice_editor.demo_notice", {
61+
date: formattedDate,
62+
});
63+
64+
// Show warning toast notification
65+
this.toasts.warning({
66+
data: { message: message },
67+
duration: 8000,
68+
});
69+
}
70+
5171
this.editorConfig = response.doc_config;
5272
this.dsHost = response.config.ds_host;
5373
this.isLoading = false;
@@ -58,6 +78,13 @@ async loadEditorConfig() {
5878
this.error = error;
5979
this.isLoading = false;
6080
popupAjaxError(error);
81+
82+
// Close modal if demo expired
83+
if (error.jqXHR?.responseJSON?.demo_expired) {
84+
setTimeout(() => {
85+
this.args.closeModal();
86+
}, 100);
87+
}
6188
}
6289
}
6390

@@ -81,7 +108,7 @@ async loadEditorConfig() {
81108
}
82109
}
83110

84-
111+
85112

86113
@action
87114
close() {
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { ajax } from "discourse/lib/ajax";
2+
import { withPluginApi } from "discourse/lib/plugin-api";
3+
4+
export default {
5+
name: "onlyoffice-demo-mode",
6+
7+
initialize(container) {
8+
const siteSettings = container.lookup("service:site-settings");
9+
10+
if (!siteSettings.onlyoffice_connector_enabled) {
11+
return;
12+
}
13+
14+
withPluginApi("0.8", (api) => {
15+
let demoInfoCache = null;
16+
17+
const applyDemoModeStyles = async () => {
18+
try {
19+
if (!demoInfoCache) {
20+
const response = await ajax("/onlyoffice/demo-info");
21+
demoInfoCache = response;
22+
}
23+
24+
const demoEnabled = demoInfoCache.enabled;
25+
const demoAvailable = demoInfoCache.available;
26+
const daysRemaining = demoInfoCache.days_remaining;
27+
const expirationDate = demoInfoCache.expiration_date;
28+
29+
// Disable settings fields when demo is enabled
30+
const settingsToDisable = [
31+
"ONLYOFFICE_Docs_address",
32+
"ONLYOFFICE_Docs_secret_key",
33+
"JWT_header",
34+
];
35+
36+
settingsToDisable.forEach((settingName) => {
37+
const settingRow = document.querySelector(
38+
`[data-setting="${settingName}"]`
39+
);
40+
41+
if (settingRow) {
42+
const input = settingRow.querySelector("input");
43+
44+
// Lock fields if demo is enabled
45+
if (input && demoEnabled) {
46+
// Make field readonly instead of disabled
47+
input.readOnly = true;
48+
input.style.opacity = "0.6";
49+
input.style.cursor = "not-allowed";
50+
input.style.backgroundColor = "#f5f5f5";
51+
input.style.pointerEvents = "none";
52+
53+
// Hide secret
54+
if (settingName === "ONLYOFFICE_Docs_secret_key") {
55+
if (!input.dataset.originalValue) {
56+
input.dataset.originalValue = input.value;
57+
}
58+
input.type = "password";
59+
input.value = input.dataset.originalValue || input.value;
60+
}
61+
} else if (input) {
62+
input.readOnly = false;
63+
input.style.opacity = "1";
64+
input.style.cursor = "text";
65+
input.style.backgroundColor = "";
66+
input.style.pointerEvents = "";
67+
68+
// Restore secret
69+
if (settingName === "ONLYOFFICE_Docs_secret_key") {
70+
input.type = "text";
71+
if (input.dataset.originalValue) {
72+
input.value = input.dataset.originalValue;
73+
delete input.dataset.originalValue;
74+
}
75+
}
76+
}
77+
}
78+
});
79+
80+
// Handle demo checkbox
81+
const demoCheckboxRow = document.querySelector(
82+
'[data-setting="Connect_to_demo_ONLYOFFICE_Docs_server"]'
83+
);
84+
if (demoCheckboxRow) {
85+
const checkbox = demoCheckboxRow.querySelector(
86+
'input[type="checkbox"]'
87+
);
88+
89+
const label = demoCheckboxRow.querySelector(".setting-label") ||
90+
demoCheckboxRow.querySelector("label");
91+
92+
// Remove old messages
93+
if (label) {
94+
const oldMessage = label.querySelector(".demo-message");
95+
if (oldMessage) {
96+
oldMessage.remove();
97+
}
98+
}
99+
100+
// Logic for expired demo mode
101+
if (!demoAvailable) {
102+
// Disable checkbox if expired and not enabled
103+
if (!demoEnabled && checkbox) {
104+
checkbox.disabled = true;
105+
}
106+
// Allow to disable it if expired and enabled
107+
else if (demoEnabled && checkbox) {
108+
checkbox.disabled = false;
109+
}
110+
111+
// Show expired message
112+
if (label) {
113+
const message = document.createElement("div");
114+
message.className = "demo-message";
115+
message.style.color = "#e74c3c";
116+
message.style.marginTop = "8px";
117+
message.style.fontSize = "13px";
118+
message.textContent = "The 30-day test period is over. You are no longer able to connect to the demo server.";
119+
label.appendChild(message);
120+
}
121+
}
122+
// Demo mode is active and enabled
123+
else if (demoEnabled && expirationDate) {
124+
const formattedDate = new Date(expirationDate).toLocaleDateString();
125+
126+
if (label) {
127+
const message = document.createElement("div");
128+
message.className = "demo-message";
129+
message.style.color = "#3498db";
130+
message.style.marginTop = "8px";
131+
message.style.fontSize = "13px";
132+
message.textContent = `You are successfully connected to the demo server. It will be available until ${formattedDate}. To disable it, uncheck the box.`;
133+
label.appendChild(message);
134+
}
135+
}
136+
137+
// Watch for demo mode changes (save and reload)
138+
if (checkbox && !checkbox.dataset.demoListenerAdded) {
139+
checkbox.dataset.demoListenerAdded = "true";
140+
141+
// Mark checkbox as changed when user toggles it
142+
checkbox.addEventListener("change", async () => {
143+
checkbox.dataset.wasChanged = "true";
144+
demoInfoCache = null; // Clear cache
145+
146+
// Find the OK button and emulated click to save
147+
const okButton = demoCheckboxRow.querySelector(".setting-controls__ok");
148+
if (okButton) {
149+
okButton.click();
150+
151+
// Wait for save
152+
setTimeout(() => {
153+
window.location.reload();
154+
}, 1500);
155+
} else {
156+
// Reload after delay
157+
setTimeout(() => {
158+
window.location.reload();
159+
}, 2000);
160+
}
161+
});
162+
}
163+
}
164+
} catch (error) {
165+
// eslint-disable-next-line no-console
166+
console.error("Failed to load demo info:", error);
167+
}
168+
};
169+
170+
const checkAndApply = () => {
171+
if (window.location.pathname.includes("/admin/site_settings") ||
172+
window.location.pathname.includes("/admin")) {
173+
setTimeout(() => applyDemoModeStyles(), 100);
174+
setTimeout(() => applyDemoModeStyles(), 500);
175+
setTimeout(() => applyDemoModeStyles(), 1000);
176+
setTimeout(() => applyDemoModeStyles(), 2000);
177+
}
178+
};
179+
180+
// Apply on page change
181+
api.onPageChange(checkAndApply);
182+
183+
// Apply if already on settings page
184+
checkAndApply();
185+
186+
setTimeout(() => {
187+
if (window.location.pathname.includes("/admin/site_settings")) {
188+
const observer = new MutationObserver(() => {
189+
applyDemoModeStyles();
190+
});
191+
192+
const settingsContainer = document.querySelector(".admin-contents");
193+
if (settingsContainer) {
194+
observer.observe(settingsContainer, {
195+
childList: true,
196+
subtree: true,
197+
});
198+
}
199+
}
200+
}, 500);
201+
});
202+
},
203+
};

0 commit comments

Comments
 (0)