security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Card } from '@/components/ui/Card'
|
||||
import { X, Send, Paperclip, Sparkles, Loader2 } from 'lucide-react'
|
||||
|
||||
interface ComposeModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
replyTo?: {
|
||||
uid: number
|
||||
subject: string
|
||||
from: string
|
||||
body: string
|
||||
}
|
||||
onSent?: () => void
|
||||
}
|
||||
|
||||
export function ComposeModal({ isOpen, onClose, replyTo, onSent }: ComposeModalProps) {
|
||||
const [to, setTo] = useState(replyTo ? extractEmail(replyTo.from) : '')
|
||||
const [subject, setSubject] = useState(replyTo ? `Re: ${replyTo.subject.replace(/^Re: /i, '')}` : '')
|
||||
const [body, setBody] = useState(replyTo ? `\n\n---\n${replyTo.body.substring(0, 500)}` : '')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [attachments, setAttachments] = useState<File[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
function extractEmail(from: string): string {
|
||||
const match = from.match(/<([^>]+)>/)
|
||||
return match ? match[1] : from
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
if (!to || !subject || !body) return
|
||||
|
||||
setSending(true)
|
||||
try {
|
||||
const token = localStorage.getItem('amos_token')
|
||||
const res = await fetch('/api/v1/mail/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: [to],
|
||||
subject,
|
||||
body,
|
||||
reply_to: replyTo?.from,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
setTo('')
|
||||
setSubject('')
|
||||
setBody('')
|
||||
setAttachments([])
|
||||
onSent?.()
|
||||
onClose()
|
||||
} else {
|
||||
alert(data.error || 'Failed to send')
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to send email')
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAIAssist() {
|
||||
if (!body.trim()) return
|
||||
setAiLoading(true)
|
||||
try {
|
||||
const token = localStorage.getItem('amos_token')
|
||||
const res = await fetch('/api/v1/mail/ai-assist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
context: body,
|
||||
tone: 'professional',
|
||||
language: 'sv',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (data.ok && data.improved) {
|
||||
setBody(data.improved)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('AI assist failed:', err)
|
||||
} finally {
|
||||
setAiLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.target.files) {
|
||||
setAttachments(Array.from(e.target.files))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<Card className="w-full max-w-2xl max-h-[90vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{replyTo ? 'Svara' : 'Nytt meddelande'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-2 rounded-lg hover:bg-bg text-text-secondary">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Till</label>
|
||||
<input
|
||||
type="email"
|
||||
value={to}
|
||||
onChange={(e) => setTo(e.target.value)}
|
||||
className="w-full h-10 px-3 rounded-xl border bg-surface text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Ämne</label>
|
||||
<input
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="w-full h-10 px-3 rounded-xl border bg-surface text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="Ämne"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Meddelande</label>
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={12}
|
||||
className="w-full px-3 py-2 rounded-xl border bg-surface text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/20 resize-none"
|
||||
placeholder="Skriv ditt meddelande..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{attachments.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-text-secondary">Bilagor</p>
|
||||
{attachments.map((file, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 bg-bg rounded-lg">
|
||||
<Paperclip size={16} className="text-text-secondary" />
|
||||
<span className="text-sm text-text-primary">{file.name}</span>
|
||||
<span className="text-xs text-text-secondary">({(file.size / 1024).toFixed(0)} KB)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
multiple
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<Paperclip size={16} />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
Bifoga
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={aiLoading ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}
|
||||
onClick={handleAIAssist}
|
||||
disabled={aiLoading || !body.trim()}
|
||||
>
|
||||
AI-hjälp
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
|
||||
onClick={handleSend}
|
||||
disabled={sending || !to || !subject || !body}
|
||||
>
|
||||
{sending ? 'Skickar...' : 'Skicka'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user