mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
Merge branch 'feature/admin-panel-3' into feature/admin-panel2
This commit is contained in:
@ -74,14 +74,14 @@ export const fetchCurrentUser = async () => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const login = async secret => {
|
||||
export const login = async ({tenantName, secret}) => {
|
||||
const response = await fetch(`${BACKEND_URL}/info/adminpanel/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({secret})
|
||||
body: JSON.stringify({tenantName, secret})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@ -7,7 +7,9 @@ import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
|
||||
import {configurationSections, ROLES} from '../../config/configurationSchema';
|
||||
import {selectUser} from '../../store/slices/userSlice';
|
||||
import ExpandableSection from '../ExpandableSection';
|
||||
import ConfigurationField from '../ConfigurationInput';
|
||||
import ConfigurationInput from '../ConfigurationInput';
|
||||
import SelectField from '../SelectField';
|
||||
import JsonField from '../JsonField';
|
||||
import Button from '../Button';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
@ -23,17 +25,6 @@ export default function Configuration() {
|
||||
// Cron expression with 6 space-separated fields (server-compatible)
|
||||
const CRON6_REGEX = /^\s*\S+(?:\s+\S+){5}\s*$/;
|
||||
|
||||
/**
|
||||
* Builds an Ajv validator instance for the provided JSON Schema.
|
||||
* @param {object} schema - Derived per-scope JSON schema
|
||||
* @returns {Ajv.ValidateFunction}
|
||||
*/
|
||||
const buildValidator = schema => {
|
||||
const ajv = new Ajv({allErrors: true, strict: false});
|
||||
ajv.addFormat('cron6', CRON6_REGEX);
|
||||
return ajv.compile(schema);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts Ajv errors to a field error map suitable for UI display.
|
||||
* @param {Ajv.ErrorObject[]} errors
|
||||
@ -72,6 +63,16 @@ export default function Configuration() {
|
||||
useEffect(() => {
|
||||
const loadConfiguration = async () => {
|
||||
try {
|
||||
/**
|
||||
* Builds an Ajv validator instance for the provided JSON Schema.
|
||||
* @param {object} schema - Derived per-scope JSON schema
|
||||
* @returns {Ajv.ValidateFunction}
|
||||
*/
|
||||
const buildValidator = schema => {
|
||||
const ajv = new Ajv({allErrors: true, strict: false});
|
||||
ajv.addFormat('cron6', CRON6_REGEX);
|
||||
return ajv.compile(schema);
|
||||
};
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
// Fetch config and schema in parallel
|
||||
@ -81,7 +82,18 @@ export default function Configuration() {
|
||||
const initialValues = {};
|
||||
filteredSections.forEach(section => {
|
||||
section.fields.forEach(field => {
|
||||
initialValues[field.path] = getNestedValue(data, field.path, '');
|
||||
let value = getNestedValue(data, field.path, '');
|
||||
|
||||
// Stringify JSON values for json type fields
|
||||
if (field.type === 'json' && value !== '') {
|
||||
try {
|
||||
value = JSON.stringify(value, null, 2);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to stringify JSON for field ${field.path}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
initialValues[field.path] = value;
|
||||
});
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
@ -97,7 +109,7 @@ export default function Configuration() {
|
||||
};
|
||||
|
||||
loadConfiguration();
|
||||
}, []);
|
||||
}, [filteredSections, CRON6_REGEX]);
|
||||
|
||||
const handleFieldChange = (path, value) => {
|
||||
setFieldValues(prev => ({
|
||||
@ -127,7 +139,23 @@ export default function Configuration() {
|
||||
|
||||
const changedObjects = section.fields.map(field => {
|
||||
const obj = {};
|
||||
obj[field.path] = fieldValues[field.path];
|
||||
let value = fieldValues[field.path];
|
||||
|
||||
// Parse JSON values for json type fields
|
||||
if (field.type === 'json') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, keep the string value and let backend validation handle it
|
||||
console.warn(`Failed to parse JSON for field ${field.path}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
value = Boolean(value);
|
||||
}
|
||||
|
||||
obj[field.path] = value;
|
||||
return obj;
|
||||
});
|
||||
|
||||
@ -149,15 +177,41 @@ export default function Configuration() {
|
||||
try {
|
||||
await updateConfiguration(mergedConfig);
|
||||
} catch (error) {
|
||||
console.log('error777', JSON.stringify(error));
|
||||
// Handle validation errors from backend
|
||||
if (error.error && error.error.details && Array.isArray(error.error.details)) {
|
||||
const errors = {};
|
||||
error.error.details.forEach(detail => {
|
||||
if (detail.path && detail.message) {
|
||||
// Join the path array to create the field path
|
||||
const fieldPath = detail.path.join('.');
|
||||
errors[fieldPath] = detail.message;
|
||||
// Find the field that contains this error path
|
||||
let fieldPath = null;
|
||||
|
||||
// Check each field in the current section to see if the error path starts with the field path
|
||||
section.fields.forEach(field => {
|
||||
const fieldPathParts = field.path.split('.');
|
||||
const errorPathParts = detail.path;
|
||||
|
||||
// Check if the error path starts with the field path
|
||||
if (fieldPathParts.length <= errorPathParts.length) {
|
||||
let matches = true;
|
||||
for (let i = 0; i < fieldPathParts.length; i++) {
|
||||
if (fieldPathParts[i] !== errorPathParts[i]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
fieldPath = field.path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we found a matching field, use it; otherwise use the full path
|
||||
if (fieldPath) {
|
||||
errors[fieldPath] = detail.message;
|
||||
} else {
|
||||
// Fallback: use the full path
|
||||
errors[detail.path.join('.')] = detail.message;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFieldErrors(prev => ({...prev, ...errors}));
|
||||
@ -182,20 +236,36 @@ export default function Configuration() {
|
||||
{filteredSections.map((section, index) => {
|
||||
return (
|
||||
<ExpandableSection key={index} title={section.title}>
|
||||
{section.fields.map(field => (
|
||||
<ConfigurationField
|
||||
key={field.path}
|
||||
label={field.label}
|
||||
value={fieldValues[field.path] || ''}
|
||||
onChange={value => handleFieldChange(field.path, value)}
|
||||
type={field.type}
|
||||
error={fieldErrors[field.path]}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
options={field.options}
|
||||
description={field.description}
|
||||
/>
|
||||
))}
|
||||
{section.fields.map(field => {
|
||||
// Select component based on type
|
||||
let FieldComponent;
|
||||
switch (field.type) {
|
||||
case 'select':
|
||||
FieldComponent = SelectField;
|
||||
break;
|
||||
case 'json':
|
||||
FieldComponent = JsonField;
|
||||
break;
|
||||
default:
|
||||
FieldComponent = ConfigurationInput;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldComponent
|
||||
key={field.path}
|
||||
label={field.label}
|
||||
value={fieldValues[field.path] || ''}
|
||||
onChange={value => handleFieldChange(field.path, value)}
|
||||
type={field.type}
|
||||
error={fieldErrors[field.path]}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
options={field.options}
|
||||
// description={field.description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button onClick={() => handleSaveSection(section.title)} errorText='FAILED'>
|
||||
SAVE
|
||||
|
||||
@ -10,32 +10,22 @@ export default function ConfigurationInput({
|
||||
error = null,
|
||||
min = null,
|
||||
max = null,
|
||||
options = [],
|
||||
description = null
|
||||
}) {
|
||||
const renderInput = () => {
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<select value={value} onChange={e => onChange(e.target.value)} className={`${styles.input} ${error ? styles.inputError : ''}`}>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return <Input type={type} value={value} onChange={onChange} placeholder={placeholder} error={error} min={min} max={max} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{label}</label>
|
||||
<div className={styles.inputContainer}>
|
||||
{renderInput()}
|
||||
{description && <div className={styles.description}>{description}</div>}
|
||||
{error && <div className={styles.errorMessage}>{error}</div>}
|
||||
<Input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
min={min}
|
||||
max={max}
|
||||
error={error}
|
||||
/>
|
||||
{/* {error && <div className={styles.errorMessage}>{error}</div>} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
font-size: 13px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.label::after {
|
||||
content: ':';
|
||||
}
|
||||
@ -27,38 +28,7 @@
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.inputError:focus {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: #dc3545;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function Input({
|
||||
_label,
|
||||
_description,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
@ -11,6 +13,18 @@ export default function Input({
|
||||
min = null,
|
||||
max = null
|
||||
}) {
|
||||
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(value)}
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
className={`${styles.input} ${error ? styles.inputError : ''} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 11px 20px;
|
||||
border: 1px solid rgb(170, 170, 170);
|
||||
@ -26,3 +39,17 @@
|
||||
.inputError {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 11px;
|
||||
color: #dc3545;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
81
AdminPanel/client/src/components/JsonField/index.js
Normal file
81
AdminPanel/client/src/components/JsonField/index.js
Normal file
@ -0,0 +1,81 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function JsonField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
error = null,
|
||||
description = null
|
||||
}) {
|
||||
const [jsonError, setJsonError] = useState(null);
|
||||
const [isValidJson, setIsValidJson] = useState(true);
|
||||
|
||||
// Validate JSON on every change
|
||||
useEffect(() => {
|
||||
if (value.trim() === '') {
|
||||
setJsonError(null);
|
||||
setIsValidJson(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
setJsonError(null);
|
||||
setIsValidJson(true);
|
||||
} catch (err) {
|
||||
setJsonError(`Invalid JSON: ${err.message}`);
|
||||
setIsValidJson(false);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const formatJson = () => {
|
||||
if (value.trim() !== '') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
onChange(formatted);
|
||||
} catch (err) {
|
||||
// If JSON is invalid, don't format
|
||||
console.warn('Cannot format invalid JSON:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const lines = value.split('\n');
|
||||
const lineNumbers = lines.map((_, index) => index + 1);
|
||||
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{label}</label>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.jsonContainer}>
|
||||
<div className={styles.lineNumbers}>
|
||||
{lineNumbers.map(num => (
|
||||
<div key={num} className={styles.lineNumber}>{num}</div>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={`${styles.textarea} ${error || jsonError ? styles.inputError : ''}`}
|
||||
rows={10}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.formatButton}
|
||||
onClick={formatJson}
|
||||
disabled={!isValidJson}
|
||||
title="Format JSON"
|
||||
>
|
||||
Format
|
||||
</button>
|
||||
</div>
|
||||
{(error || jsonError) && <div className={styles.errorMessage}>{error || jsonError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
AdminPanel/client/src/components/JsonField/styles.module.css
Normal file
116
AdminPanel/client/src/components/JsonField/styles.module.css
Normal file
@ -0,0 +1,116 @@
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
color: rgb(68, 68, 68);
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.label::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.jsonContainer {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lineNumbers {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ccc;
|
||||
border-right: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding: 8px 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #6c757d;
|
||||
user-select: none;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lineNumber {
|
||||
height: 1.4em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-left: none;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.jsonContainer:focus-within .lineNumbers {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.formatButton {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.formatButton:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.formatButton:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: #dc3545;
|
||||
font-size: 12px;
|
||||
}
|
||||
30
AdminPanel/client/src/components/SelectField/index.js
Normal file
30
AdminPanel/client/src/components/SelectField/index.js
Normal file
@ -0,0 +1,30 @@
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function SelectField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options = [],
|
||||
error = null,
|
||||
description = null
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{label}</label>
|
||||
<div className={styles.inputContainer}>
|
||||
<select
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className={`${styles.select} ${error ? styles.inputError : ''}`}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* {error && <div className={styles.errorMessage}>{error}</div>} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
color: rgb(68, 68, 68);
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.label::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.inputError:focus {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: #dc3545;
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -37,6 +37,20 @@ export const configurationSections = [
|
||||
min: 0,
|
||||
roles: [ROLES.ADMIN],
|
||||
description: 'Number of files to remove at once (admin only)'
|
||||
},
|
||||
{
|
||||
path: 'services.CoAuthoring.expire.sessionidle',
|
||||
label: 'Session Idle Timeout',
|
||||
type: 'text',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Session idle timeout (e.g., "1h", "30m")'
|
||||
},
|
||||
{
|
||||
path: 'services.CoAuthoring.expire.sessionabsolute',
|
||||
label: 'Session Absolute Timeout',
|
||||
type: 'text',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Session absolute timeout (e.g., "30d", "24h")'
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -70,6 +84,88 @@ export const configurationSections = [
|
||||
max: 104857600,
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Maximum number of bytes allowed for download (max: 100MB)'
|
||||
},
|
||||
{
|
||||
path: 'FileConverter.converter.inputLimits',
|
||||
label: 'Input Limits',
|
||||
type: 'json',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'File type limits for conversion. Format: [{"type": "docx;dotx", "zip": {"uncompressed": "50MB", "template": "*.xml"}}]'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WOPI Configuration',
|
||||
fields: [
|
||||
{
|
||||
path: 'wopi.enable',
|
||||
label: 'Enable WOPI',
|
||||
type: 'checkbox',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Enable WOPI (Web Application Open Platform Interface) support'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Email Configuration',
|
||||
fields: [
|
||||
{
|
||||
path: 'email.smtpServerConfiguration.host',
|
||||
label: 'SMTP Host',
|
||||
type: 'text',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'SMTP server hostname'
|
||||
},
|
||||
{
|
||||
path: 'email.smtpServerConfiguration.port',
|
||||
label: 'SMTP Port',
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'SMTP server port number'
|
||||
},
|
||||
{
|
||||
path: 'email.smtpServerConfiguration.auth.user',
|
||||
label: 'SMTP Username',
|
||||
type: 'text',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'SMTP authentication username'
|
||||
},
|
||||
{
|
||||
path: 'email.smtpServerConfiguration.auth.pass',
|
||||
label: 'SMTP Password',
|
||||
type: 'password',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'SMTP authentication password'
|
||||
},
|
||||
{
|
||||
path: 'email.connectionConfiguration.disableFileAccess',
|
||||
label: 'Disable File Access',
|
||||
type: 'checkbox',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Disable file access for email connections'
|
||||
},
|
||||
{
|
||||
path: 'email.connectionConfiguration.disableUrlAccess',
|
||||
label: 'Disable URL Access',
|
||||
type: 'checkbox',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Disable URL access for email connections'
|
||||
},
|
||||
{
|
||||
path: 'email.contactDefaults.from',
|
||||
label: 'Default From Email',
|
||||
type: 'email',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Default sender email address'
|
||||
},
|
||||
{
|
||||
path: 'email.contactDefaults.to',
|
||||
label: 'Default To Email',
|
||||
type: 'email',
|
||||
roles: [ROLES.ADMIN, ROLES.USER],
|
||||
description: 'Default recipient email address'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import Button from '../../components/Button';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function Login() {
|
||||
const [tenantName, setTenantName] = useState('');
|
||||
const [secret, setSecret] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
@ -15,7 +16,7 @@ export default function Login() {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await dispatch(loginUser(secret)).unwrap();
|
||||
await dispatch(loginUser({tenantName, secret})).unwrap();
|
||||
} catch (error) {
|
||||
setError(error || 'Invalid credentials. Please try again.');
|
||||
throw error; // Re-throw to trigger error state in Button component
|
||||
@ -40,11 +41,22 @@ export default function Login() {
|
||||
<div className={styles.form}>
|
||||
<div className={styles.inputGroup}>
|
||||
<Input
|
||||
label='Secret Key'
|
||||
type='text'
|
||||
value={tenantName}
|
||||
onChange={setTenantName}
|
||||
placeholder='Enter your tenant name'
|
||||
description='The name of your tenant organization'
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputGroup}>
|
||||
<Input
|
||||
type='password'
|
||||
value={secret}
|
||||
onChange={setSecret}
|
||||
placeholder='Enter your secret key'
|
||||
description='The secret key associated with your tenant'
|
||||
error={error}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
@ -9,9 +9,9 @@ export const fetchUser = createAsyncThunk('user/fetchUser', async (_, {rejectWit
|
||||
}
|
||||
});
|
||||
|
||||
export const loginUser = createAsyncThunk('user/loginUser', async (secret, {rejectWithValue}) => {
|
||||
export const loginUser = createAsyncThunk('user/loginUser', async ({tenantName, secret}, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await login(secret);
|
||||
const response = await login({tenantName, secret});
|
||||
return response;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
|
||||
Reference in New Issue
Block a user