add kerio format support
This commit is contained in:
@@ -177,3 +177,13 @@ export const importCSV = (id: number, file: File) => {
|
||||
fd.append('file', file)
|
||||
return api<{ imported: number }>(`/api/tasks/${id}/import`, { method: 'POST', body: fd })
|
||||
}
|
||||
|
||||
// A Kerio Connect export carries no domain, so the operator supplies it here;
|
||||
// the login and password apply to both sides of the migration.
|
||||
export const importKerioCSV = (id: number, file: File, domain: string) => {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('format', 'kerio')
|
||||
fd.append('domain', domain)
|
||||
return api<{ imported: number }>(`/api/tasks/${id}/import`, { method: 'POST', body: fd })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import { Modal } from './Modal'
|
||||
|
||||
// A bare DNS domain: no scheme, no user part, no whitespace. Mirrors the
|
||||
// server-side check in csvimport.normalizeDomain so bad input is caught here.
|
||||
const domainRe = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
busy: boolean
|
||||
onClose: () => void
|
||||
onSubmit: (file: File, domain: string) => void
|
||||
}
|
||||
|
||||
export function KerioImportModal({ open, busy, onClose, onSubmit }: Props) {
|
||||
const [domain, setDomain] = useState('')
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setDomain('')
|
||||
setFile(null)
|
||||
setError(null)
|
||||
}, [open])
|
||||
|
||||
function submit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
const d = domain.trim().toLowerCase()
|
||||
if (!domainRe.test(d)) {
|
||||
setError('Enter a bare domain, e.g. galaxyhotel.kz')
|
||||
return
|
||||
}
|
||||
if (!file) {
|
||||
setError('Choose the Kerio export file')
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
onSubmit(file, d)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} title="Import from Kerio" onClose={onClose}>
|
||||
<form onSubmit={submit}>
|
||||
<p className="map-hint">
|
||||
The Kerio user export lists a login and its password but no domain. The domain you enter is appended to every
|
||||
login and used for both the source and the destination. Disabled accounts and <code>admin</code> are skipped.
|
||||
</p>
|
||||
<div className="field">
|
||||
<label htmlFor="kerio_domain">Mail domain</label>
|
||||
<input
|
||||
id="kerio_domain"
|
||||
data-modal-autofocus
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder="galaxyhotel.kz"
|
||||
disabled={busy}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="kerio_file">Export file</label>
|
||||
<input
|
||||
id="kerio_file"
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
||||
disabled={busy}
|
||||
/>
|
||||
</div>
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn" onClick={onClose} disabled={busy}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-primary" disabled={busy}>
|
||||
{busy ? 'Importing…' : 'Import'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from 'react'
|
||||
import { cancelAccount, createAccount, deleteAccount, getTask, importCSV, probeAccountFolders, probeFolders, runTask, setAccountFolderMapping, setTaskSchedule, testAccounts, type TaskDetail as TaskDetailData } from '../api'
|
||||
import { cancelAccount, createAccount, deleteAccount, getTask, importCSV, importKerioCSV, probeAccountFolders, probeFolders, runTask, setAccountFolderMapping, setTaskSchedule, testAccounts, type TaskDetail as TaskDetailData } from '../api'
|
||||
import { connectTaskWS, type TaskEvent } from '../ws'
|
||||
import { StatusBadge } from '../components/StatusBadge'
|
||||
import { useConfirm } from '../components/ConfirmProvider'
|
||||
import { FolderMappingModal } from '../components/FolderMappingModal'
|
||||
import { RunLogModal } from '../components/RunLogModal'
|
||||
import { AccountErrorsModal } from '../components/AccountErrorsModal'
|
||||
import { KerioImportModal } from '../components/KerioImportModal'
|
||||
|
||||
const emptyAccount = { src_login: '', src_pass: '', dst_login: '', dst_pass: '' }
|
||||
|
||||
@@ -90,6 +91,7 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
const [live, setLive] = useState<Record<number, LiveProgress>>({})
|
||||
const [showRuns, setShowRuns] = useState(false)
|
||||
const [errorsFor, setErrorsFor] = useState<{ id: number; src_login: string } | null>(null)
|
||||
const [kerioOpen, setKerioOpen] = useState(false)
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set())
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -288,6 +290,20 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onKerioImport(file: File, domain: string) {
|
||||
setBusy('import')
|
||||
setError(null)
|
||||
try {
|
||||
await importKerioCSV(id, file, domain)
|
||||
setKerioOpen(false)
|
||||
reload()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Kerio import failed')
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteAccount(accId: number, login: string) {
|
||||
const ok = await confirm({
|
||||
title: 'Remove account',
|
||||
@@ -544,6 +560,9 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
{busy === 'import' ? 'Importing…' : 'Upload CSV'}
|
||||
<input ref={fileInputRef} type="file" accept=".csv,text/csv" onChange={onFileChosen} disabled={busy !== null} />
|
||||
</label>
|
||||
<button type="button" className="btn" onClick={() => setKerioOpen(true)} disabled={busy !== null}>
|
||||
Import from Kerio
|
||||
</button>
|
||||
<button type="button" className="link-btn" onClick={downloadExampleCSV}>
|
||||
download example.csv
|
||||
</button>
|
||||
@@ -768,6 +787,12 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
)}
|
||||
<RunLogModal taskId={id} open={showRuns} onClose={() => setShowRuns(false)} />
|
||||
<AccountErrorsModal taskId={id} account={errorsFor} onClose={() => setErrorsFor(null)} />
|
||||
<KerioImportModal
|
||||
open={kerioOpen}
|
||||
busy={busy === 'import'}
|
||||
onClose={() => setKerioOpen(false)}
|
||||
onSubmit={onKerioImport}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user