Admins previously had no UI path to provision per-peer accounts for
email, calendar, and files: they had to hit the AccountManager API
routes directly. This change wires those routes to a dedicated Accounts
tab on each service page so any peer can be granted or revoked service
access in two clicks.
- webui/src/services/api.js: add accountsAPI with list/provision/
deprovision/getCredentials, pointing to
/api/services/catalog/{serviceId}/accounts
- webui/src/components/ServiceAccountsPanel.jsx: new reusable panel;
handles credential reveal, removal confirmation, load-error state,
and humanized credential labels
- EmailPage, CalendarPage, FilesPage: Overview/Accounts tab nav (admin
only); Accounts tab renders ServiceAccountsPanel; AdminConfigSection
is hidden while on the Accounts tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useDraftConfig } from '../../contexts/DraftConfigContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Field, TextInput, NumberInput } from '../../components/FormFields';
|
||||
import { detectPortConflicts, validateCalendarConfig, PORT_CONFLICT_FIELDS } from '../../utils/serviceConfig';
|
||||
import ServiceAccountsPanel from '../../components/ServiceAccountsPanel';
|
||||
|
||||
const CAL_DEFAULTS = { port: 5232, data_dir: '' };
|
||||
|
||||
@@ -68,6 +69,7 @@ function Toast({ msg, type }) {
|
||||
export default function CalendarPage() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {}, refresh: refreshConfig } = useConfig();
|
||||
const draftConfig = useDraftConfig();
|
||||
|
||||
@@ -160,11 +162,31 @@ export default function CalendarPage() {
|
||||
<div>
|
||||
<Toast {...(toast || {})} />
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Calendar & Contacts</h1>
|
||||
<p className="mt-2 text-gray-600">Radicale CalDAV / CardDAV server</p>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mb-6 border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-6 overflow-x-auto">
|
||||
{[{id:'overview',label:'Overview'},{id:'accounts',label:'Accounts'}].map(tab => (
|
||||
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
|
||||
className={`whitespace-nowrap px-1 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'accounts' && isAdmin ? (
|
||||
<ServiceAccountsPanel serviceId="calendar" serviceName="Calendar" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Connection info */}
|
||||
<div className="card">
|
||||
@@ -287,7 +309,10 @@ export default function CalendarPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab !== 'accounts' && (
|
||||
<AdminConfigSection
|
||||
calCfg={calCfg}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDraftConfig } from '../../contexts/DraftConfigContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Field, TextInput, NumberInput } from '../../components/FormFields';
|
||||
import { detectPortConflicts, validateEmailConfig, PORT_CONFLICT_FIELDS } from '../../utils/serviceConfig';
|
||||
import ServiceAccountsPanel from '../../components/ServiceAccountsPanel';
|
||||
|
||||
const EMAIL_DEFAULTS = { domain: '', smtp_port: 25, submission_port: 587, imap_port: 993, webmail_port: 8888 };
|
||||
|
||||
@@ -78,6 +79,7 @@ function Toast({ msg, type }) {
|
||||
export default function EmailPage() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {}, refresh: refreshConfig } = useConfig();
|
||||
const draftConfig = useDraftConfig();
|
||||
|
||||
@@ -182,11 +184,31 @@ export default function EmailPage() {
|
||||
<div>
|
||||
<Toast {...(toast || {})} />
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Email Services</h1>
|
||||
<p className="mt-2 text-gray-600">Postfix (SMTP) + Dovecot (IMAP)</p>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mb-6 border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-6 overflow-x-auto">
|
||||
{[{id:'overview',label:'Overview'},{id:'accounts',label:'Accounts'}].map(tab => (
|
||||
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
|
||||
className={`whitespace-nowrap px-1 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'accounts' && isAdmin ? (
|
||||
<ServiceAccountsPanel serviceId="email" serviceName="Email" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* IMAP */}
|
||||
<div className="card">
|
||||
@@ -291,8 +313,10 @@ export default function EmailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Admin config form */}
|
||||
{isAdmin && (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab !== 'accounts' && (
|
||||
<AdminConfigSection
|
||||
emailCfg={emailCfg}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDraftConfig } from '../../contexts/DraftConfigContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Field, TextInput, NumberInput } from '../../components/FormFields';
|
||||
import { detectPortConflicts, validateFilesConfig, PORT_CONFLICT_FIELDS } from '../../utils/serviceConfig';
|
||||
import ServiceAccountsPanel from '../../components/ServiceAccountsPanel';
|
||||
|
||||
const FILES_DEFAULTS = { port: 8080, manager_port: 8082, data_dir: '', quota: 1024 };
|
||||
|
||||
@@ -75,6 +76,7 @@ function Toast({ msg, type }) {
|
||||
export default function FilesPage() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {}, refresh: refreshConfig } = useConfig();
|
||||
const draftConfig = useDraftConfig();
|
||||
|
||||
@@ -171,11 +173,31 @@ export default function FilesPage() {
|
||||
<div>
|
||||
<Toast {...(toast || {})} />
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">File Storage</h1>
|
||||
<p className="mt-2 text-gray-600">FileGator (browser) + WebDAV (native clients)</p>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mb-6 border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-6 overflow-x-auto">
|
||||
{[{id:'overview',label:'Overview'},{id:'accounts',label:'Accounts'}].map(tab => (
|
||||
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
|
||||
className={`whitespace-nowrap px-1 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'accounts' && isAdmin ? (
|
||||
<ServiceAccountsPanel serviceId="files" serviceName="Files" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* File manager */}
|
||||
<div className="card">
|
||||
@@ -291,7 +313,10 @@ export default function FilesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab !== 'accounts' && (
|
||||
<AdminConfigSection
|
||||
filesCfg={filesCfg}
|
||||
onChange={handleChange}
|
||||
|
||||
Reference in New Issue
Block a user