@hed-hog/core 0.0.321 → 0.0.325

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/auth/auth.controller.d.ts +3 -3
  2. package/dist/auth/auth.service.d.ts +8 -8
  3. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  4. package/dist/auth/guards/auth.guard.js +0 -1
  5. package/dist/auth/guards/auth.guard.js.map +1 -1
  6. package/dist/core.module.d.ts.map +1 -1
  7. package/dist/core.module.js +3 -0
  8. package/dist/core.module.js.map +1 -1
  9. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  10. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  11. package/dist/file/file.controller.d.ts +2 -2
  12. package/dist/file/file.service.d.ts +4 -4
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/mail-profile/dto/create.dto.d.ts +23 -0
  18. package/dist/mail-profile/dto/create.dto.d.ts.map +1 -0
  19. package/dist/mail-profile/dto/create.dto.js +139 -0
  20. package/dist/mail-profile/dto/create.dto.js.map +1 -0
  21. package/dist/mail-profile/dto/update.dto.d.ts +6 -0
  22. package/dist/mail-profile/dto/update.dto.d.ts.map +1 -0
  23. package/dist/mail-profile/dto/update.dto.js +9 -0
  24. package/dist/mail-profile/dto/update.dto.js.map +1 -0
  25. package/dist/mail-profile/mail-profile.controller.d.ts +94 -0
  26. package/dist/mail-profile/mail-profile.controller.d.ts.map +1 -0
  27. package/dist/mail-profile/mail-profile.controller.js +95 -0
  28. package/dist/mail-profile/mail-profile.controller.js.map +1 -0
  29. package/dist/mail-profile/mail-profile.module.d.ts +3 -0
  30. package/dist/mail-profile/mail-profile.module.d.ts.map +1 -0
  31. package/dist/mail-profile/mail-profile.module.js +31 -0
  32. package/dist/mail-profile/mail-profile.module.js.map +1 -0
  33. package/dist/mail-profile/mail-profile.service.d.ts +109 -0
  34. package/dist/mail-profile/mail-profile.service.d.ts.map +1 -0
  35. package/dist/mail-profile/mail-profile.service.js +278 -0
  36. package/dist/mail-profile/mail-profile.service.js.map +1 -0
  37. package/dist/session/session.controller.d.ts +1 -1
  38. package/dist/session/session.service.d.ts +3 -3
  39. package/dist/user/user.controller.d.ts +18 -3
  40. package/dist/user/user.controller.d.ts.map +1 -1
  41. package/dist/user/user.controller.js +22 -0
  42. package/dist/user/user.controller.js.map +1 -1
  43. package/dist/user/user.service.d.ts +22 -7
  44. package/dist/user/user.service.d.ts.map +1 -1
  45. package/dist/user/user.service.js +99 -0
  46. package/dist/user/user.service.js.map +1 -1
  47. package/hedhog/data/menu.yaml +35 -3
  48. package/hedhog/data/route.yaml +64 -0
  49. package/hedhog/frontend/app/mail/page.tsx.ejs +5 -0
  50. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +825 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +25 -1383
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +109 -55
  53. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +304 -0
  54. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +1385 -0
  55. package/hedhog/frontend/messages/en.json +91 -0
  56. package/hedhog/frontend/messages/pt.json +91 -0
  57. package/hedhog/table/mail_profile.yaml +75 -0
  58. package/package.json +4 -4
  59. package/src/auth/guards/auth.guard.ts +0 -2
  60. package/src/core.module.ts +3 -0
  61. package/src/index.ts +2 -0
  62. package/src/mail-profile/dto/create.dto.ts +145 -0
  63. package/src/mail-profile/dto/update.dto.ts +4 -0
  64. package/src/mail-profile/mail-profile.controller.ts +56 -0
  65. package/src/mail-profile/mail-profile.module.ts +18 -0
  66. package/src/mail-profile/mail-profile.service.ts +308 -0
  67. package/src/user/user.controller.ts +13 -0
  68. package/src/user/user.service.ts +98 -5
@@ -0,0 +1,825 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+
5
+ import {
6
+ EmptyState,
7
+ Page,
8
+ PageHeader,
9
+ PaginationFooter,
10
+ SearchBar,
11
+ } from '@/components/entity-list';
12
+ import {
13
+ AlertDialog,
14
+ AlertDialogAction,
15
+ AlertDialogCancel,
16
+ AlertDialogContent,
17
+ AlertDialogDescription,
18
+ AlertDialogFooter,
19
+ AlertDialogHeader,
20
+ AlertDialogTitle,
21
+ } from '@/components/ui/alert-dialog';
22
+ import { Badge } from '@/components/ui/badge';
23
+ import { Button } from '@/components/ui/button';
24
+ import { Input } from '@/components/ui/input';
25
+ import { Label } from '@/components/ui/label';
26
+ import {
27
+ Select,
28
+ SelectContent,
29
+ SelectItem,
30
+ SelectTrigger,
31
+ SelectValue,
32
+ } from '@/components/ui/select';
33
+ import {
34
+ Sheet,
35
+ SheetContent,
36
+ SheetDescription,
37
+ SheetHeader,
38
+ SheetTitle,
39
+ } from '@/components/ui/sheet';
40
+ import { Switch } from '@/components/ui/switch';
41
+ import {
42
+ Table,
43
+ TableBody,
44
+ TableCell,
45
+ TableHead,
46
+ TableHeader,
47
+ TableRow,
48
+ } from '@/components/ui/table';
49
+ import { usePagination } from '@/hooks/use-pagination';
50
+ import { formatDate } from '@/lib/format-date';
51
+ import { useApp } from '@hed-hog/next-app-provider';
52
+ import { Mail, Pencil, Send, Trash2 } from 'lucide-react';
53
+ import { useTranslations } from 'next-intl';
54
+ import { useEffect, useState } from 'react';
55
+ import { toast } from 'sonner';
56
+
57
+ type ProviderType = 'SMTP' | 'GMAIL' | 'SES';
58
+
59
+ type MailProfile = {
60
+ id: number;
61
+ slug: string;
62
+ name: string;
63
+ provider: ProviderType;
64
+ from_name?: string | null;
65
+ from_email: string;
66
+ reply_to_name?: string | null;
67
+ reply_to_email?: string | null;
68
+ smtp_host?: string | null;
69
+ smtp_port?: number | null;
70
+ smtp_secure?: boolean;
71
+ smtp_username?: string | null;
72
+ smtp_password?: string | null;
73
+ gmail_client_id?: string | null;
74
+ gmail_client_secret?: string | null;
75
+ gmail_refresh_token?: string | null;
76
+ ses_access_key_id?: string | null;
77
+ ses_secret_access_key?: string | null;
78
+ ses_region?: string | null;
79
+ created_at: string;
80
+ updated_at: string;
81
+ };
82
+
83
+ type MailProfileForm = {
84
+ slug: string;
85
+ name: string;
86
+ provider: ProviderType;
87
+ from_name: string;
88
+ from_email: string;
89
+ reply_to_name: string;
90
+ reply_to_email: string;
91
+ smtp_host: string;
92
+ smtp_port: string;
93
+ smtp_secure: boolean;
94
+ smtp_username: string;
95
+ smtp_password: string;
96
+ gmail_client_id: string;
97
+ gmail_client_secret: string;
98
+ gmail_refresh_token: string;
99
+ ses_access_key_id: string;
100
+ ses_secret_access_key: string;
101
+ ses_region: string;
102
+ };
103
+
104
+ const defaultFormData = (): MailProfileForm => ({
105
+ slug: '',
106
+ name: '',
107
+ provider: 'SMTP',
108
+ from_name: '',
109
+ from_email: '',
110
+ reply_to_name: '',
111
+ reply_to_email: '',
112
+ smtp_host: '',
113
+ smtp_port: '587',
114
+ smtp_secure: true,
115
+ smtp_username: '',
116
+ smtp_password: '',
117
+ gmail_client_id: '',
118
+ gmail_client_secret: '',
119
+ gmail_refresh_token: '',
120
+ ses_access_key_id: '',
121
+ ses_secret_access_key: '',
122
+ ses_region: '',
123
+ });
124
+
125
+ export default function MailProfilesPage() {
126
+ const t = useTranslations('core.MailProfilePage');
127
+ const { request, currentLocaleCode, getSettingValue } = useApp();
128
+ const {
129
+ items,
130
+ refetch,
131
+ page,
132
+ setPage,
133
+ pageSize,
134
+ setPageSize,
135
+ totalItems,
136
+ search,
137
+ setSearch,
138
+ } = usePagination({ url: '/mail-profile' });
139
+
140
+ const [profiles, setProfiles] = useState<MailProfile[]>([]);
141
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
142
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
143
+ const [profileToDelete, setProfileToDelete] = useState<number | null>(null);
144
+ const [selectedProfile, setSelectedProfile] = useState<MailProfile | null>(
145
+ null
146
+ );
147
+ const [editMode, setEditMode] = useState(false);
148
+ const [formData, setFormData] = useState<MailProfileForm>(defaultFormData());
149
+ const [isTesting, setIsTesting] = useState(false);
150
+ const [testedSignature, setTestedSignature] = useState<string | null>(null);
151
+
152
+ useEffect(() => {
153
+ setProfiles(Array.isArray(items) ? (items as MailProfile[]) : []);
154
+ }, [items]);
155
+
156
+ const handleCreate = () => {
157
+ setEditMode(false);
158
+ setSelectedProfile(null);
159
+ setFormData(defaultFormData());
160
+ setTestedSignature(null);
161
+ setIsDialogOpen(true);
162
+ };
163
+
164
+ const handleEdit = async (profile: MailProfile) => {
165
+ setEditMode(true);
166
+ setSelectedProfile(profile);
167
+ setTestedSignature(null);
168
+ setIsDialogOpen(true);
169
+
170
+ try {
171
+ const { data } = await request<MailProfile>({
172
+ url: `/mail-profile/${profile.id}`,
173
+ method: 'GET',
174
+ });
175
+
176
+ setFormData({
177
+ slug: data.slug ?? '',
178
+ name: data.name ?? '',
179
+ provider: data.provider ?? 'SMTP',
180
+ from_name: data.from_name ?? '',
181
+ from_email: data.from_email ?? '',
182
+ reply_to_name: data.reply_to_name ?? '',
183
+ reply_to_email: data.reply_to_email ?? '',
184
+ smtp_host: data.smtp_host ?? '',
185
+ smtp_port: data.smtp_port ? String(data.smtp_port) : '587',
186
+ smtp_secure: Boolean(data.smtp_secure),
187
+ smtp_username: data.smtp_username ?? '',
188
+ smtp_password: data.smtp_password ?? '',
189
+ gmail_client_id: data.gmail_client_id ?? '',
190
+ gmail_client_secret: data.gmail_client_secret ?? '',
191
+ gmail_refresh_token: data.gmail_refresh_token ?? '',
192
+ ses_access_key_id: data.ses_access_key_id ?? '',
193
+ ses_secret_access_key: data.ses_secret_access_key ?? '',
194
+ ses_region: data.ses_region ?? '',
195
+ });
196
+ } catch (error) {
197
+ console.error('Error loading mail profile:', error);
198
+ toast.error(t('loadError'));
199
+ }
200
+ };
201
+
202
+ const handleDelete = (id: number) => {
203
+ setProfileToDelete(id);
204
+ setIsDeleteDialogOpen(true);
205
+ };
206
+
207
+ const confirmDelete = async () => {
208
+ if (profileToDelete === null) {
209
+ return;
210
+ }
211
+
212
+ try {
213
+ await request({
214
+ url: '/mail-profile',
215
+ method: 'DELETE',
216
+ data: { ids: [profileToDelete] },
217
+ });
218
+ toast.success(t('deleteSuccess'));
219
+ setProfileToDelete(null);
220
+ setIsDeleteDialogOpen(false);
221
+ refetch();
222
+ } catch (error) {
223
+ console.error('Error deleting mail profile:', error);
224
+ toast.error(t('deleteError'));
225
+ }
226
+ };
227
+
228
+ const updateFormField = <K extends keyof MailProfileForm>(
229
+ field: K,
230
+ value: MailProfileForm[K]
231
+ ) => {
232
+ setFormData((current) => ({
233
+ ...current,
234
+ [field]: value,
235
+ }));
236
+ };
237
+
238
+ const handleProviderChange = (provider: ProviderType) => {
239
+ setFormData((current) => ({
240
+ ...current,
241
+ provider,
242
+ }));
243
+ };
244
+
245
+ const createPayload = () => ({
246
+ slug: formData.slug,
247
+ name: formData.name,
248
+ provider: formData.provider,
249
+ from_name: formData.from_name || undefined,
250
+ from_email: formData.from_email,
251
+ reply_to_name: formData.reply_to_name || undefined,
252
+ reply_to_email: formData.reply_to_email || undefined,
253
+ smtp_host: formData.smtp_host || undefined,
254
+ smtp_port: formData.smtp_port ? Number(formData.smtp_port) : undefined,
255
+ smtp_secure: formData.smtp_secure,
256
+ smtp_username: formData.smtp_username || undefined,
257
+ smtp_password: formData.smtp_password || undefined,
258
+ gmail_client_id: formData.gmail_client_id || undefined,
259
+ gmail_client_secret: formData.gmail_client_secret || undefined,
260
+ gmail_refresh_token: formData.gmail_refresh_token || undefined,
261
+ ses_access_key_id: formData.ses_access_key_id || undefined,
262
+ ses_secret_access_key: formData.ses_secret_access_key || undefined,
263
+ ses_region: formData.ses_region || undefined,
264
+ });
265
+
266
+ const currentSignature = JSON.stringify(createPayload());
267
+
268
+ const canSubmitProfile = testedSignature === currentSignature;
269
+
270
+ const handleTest = async () => {
271
+ const payload = createPayload();
272
+
273
+ try {
274
+ setIsTesting(true);
275
+ const { data } = await request<{ success: boolean; destination: string }>({
276
+ url: '/mail-profile/test',
277
+ method: 'POST',
278
+ data: payload,
279
+ });
280
+
281
+ setTestedSignature(JSON.stringify(payload));
282
+ toast.success(
283
+ t('testSuccess', {
284
+ email: data.destination || payload.from_email,
285
+ })
286
+ );
287
+ } catch (error) {
288
+ console.error('Error testing mail profile:', error);
289
+ setTestedSignature(null);
290
+ toast.error(t('testError'));
291
+ } finally {
292
+ setIsTesting(false);
293
+ }
294
+ };
295
+
296
+ const handleSubmit = async (event: React.FormEvent) => {
297
+ event.preventDefault();
298
+
299
+ const payload = createPayload();
300
+
301
+ try {
302
+ if (editMode && selectedProfile) {
303
+ await request({
304
+ url: `/mail-profile/${selectedProfile.id}`,
305
+ method: 'PATCH',
306
+ data: payload,
307
+ });
308
+ toast.success(t('updateSuccess'));
309
+ } else {
310
+ await request({
311
+ url: '/mail-profile',
312
+ method: 'POST',
313
+ data: payload,
314
+ });
315
+ toast.success(t('createSuccess'));
316
+ }
317
+
318
+ setIsDialogOpen(false);
319
+ setTestedSignature(null);
320
+ refetch();
321
+ } catch (error) {
322
+ console.error('Error saving mail profile:', error);
323
+ toast.error(editMode ? t('updateError') : t('createError'));
324
+ }
325
+ };
326
+
327
+ const renderSender = (profile: MailProfile) => {
328
+ if (profile.from_name) {
329
+ return `${profile.from_name} <${profile.from_email}>`;
330
+ }
331
+
332
+ return profile.from_email;
333
+ };
334
+
335
+ const renderReplyTo = (profile: MailProfile) => {
336
+ if (!profile.reply_to_email) {
337
+ return t('notConfigured');
338
+ }
339
+
340
+ if (profile.reply_to_name) {
341
+ return `${profile.reply_to_name} <${profile.reply_to_email}>`;
342
+ }
343
+
344
+ return profile.reply_to_email;
345
+ };
346
+
347
+ return (
348
+ <Page>
349
+ <PageHeader
350
+ breadcrumbs={[
351
+ { label: t('breadcrumbHome'), href: '/' },
352
+ { label: t('breadcrumbTitle') },
353
+ ]}
354
+ actions={[
355
+ {
356
+ label: t('newProfile'),
357
+ onClick: handleCreate,
358
+ variant: 'default' as const,
359
+ },
360
+ ]}
361
+ title={t('title')}
362
+ description={t('description')}
363
+ />
364
+
365
+ <SearchBar
366
+ searchQuery={search}
367
+ onSearchChange={(value) => {
368
+ setSearch(value);
369
+ setPage(1);
370
+ }}
371
+ onSearch={() => setPage(1)}
372
+ placeholder={t('searchPlaceholder')}
373
+ />
374
+
375
+ {profiles.length === 0 ? (
376
+ <EmptyState
377
+ icon={<Send className="h-12 w-12" />}
378
+ title={t('noProfilesFound')}
379
+ description={t('noProfilesHint')}
380
+ actionLabel={t('newProfile')}
381
+ onAction={handleCreate}
382
+ />
383
+ ) : (
384
+ <div className="rounded-md border">
385
+ <Table>
386
+ <TableHeader>
387
+ <TableRow>
388
+ <TableHead>{t('tableName')}</TableHead>
389
+ <TableHead>{t('tableSlug')}</TableHead>
390
+ <TableHead>{t('tableProvider')}</TableHead>
391
+ <TableHead>{t('tableSender')}</TableHead>
392
+ <TableHead>{t('tableReplyTo')}</TableHead>
393
+ <TableHead>{t('tableUpdated')}</TableHead>
394
+ <TableHead className="text-right">
395
+ {t('tableActions')}
396
+ </TableHead>
397
+ </TableRow>
398
+ </TableHeader>
399
+ <TableBody>
400
+ {profiles.map((profile) => (
401
+ <TableRow
402
+ key={profile.id}
403
+ className="cursor-pointer"
404
+ onDoubleClick={() => handleEdit(profile)}
405
+ >
406
+ <TableCell className="font-medium">{profile.name}</TableCell>
407
+ <TableCell className="font-mono text-sm">
408
+ {profile.slug}
409
+ </TableCell>
410
+ <TableCell>
411
+ <Badge variant="secondary">{profile.provider}</Badge>
412
+ </TableCell>
413
+ <TableCell>{renderSender(profile)}</TableCell>
414
+ <TableCell>{renderReplyTo(profile)}</TableCell>
415
+ <TableCell className="text-muted-foreground">
416
+ {formatDate(
417
+ profile.updated_at,
418
+ getSettingValue,
419
+ currentLocaleCode
420
+ )}
421
+ </TableCell>
422
+ <TableCell className="text-right">
423
+ <div className="flex justify-end gap-2">
424
+ <Button
425
+ variant="ghost"
426
+ size="icon"
427
+ onClick={(event) => {
428
+ event.stopPropagation();
429
+ handleEdit(profile);
430
+ }}
431
+ >
432
+ <Pencil className="h-4 w-4" />
433
+ </Button>
434
+ <Button
435
+ variant="ghost"
436
+ size="icon"
437
+ onClick={(event) => {
438
+ event.stopPropagation();
439
+ handleDelete(profile.id);
440
+ }}
441
+ >
442
+ <Trash2 className="h-4 w-4 text-destructive" />
443
+ </Button>
444
+ </div>
445
+ </TableCell>
446
+ </TableRow>
447
+ ))}
448
+ </TableBody>
449
+ </Table>
450
+ </div>
451
+ )}
452
+
453
+ <PaginationFooter
454
+ currentPage={page}
455
+ pageSize={pageSize}
456
+ totalItems={totalItems}
457
+ onPageChange={setPage}
458
+ onPageSizeChange={(newSize) => {
459
+ setPageSize(newSize);
460
+ setPage(1);
461
+ }}
462
+ />
463
+
464
+ <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
465
+ <SheetContent
466
+ side="right"
467
+ className="w-full max-w-[95vw] overflow-y-auto sm:max-w-4xl"
468
+ >
469
+ <SheetHeader>
470
+ <SheetTitle>
471
+ {editMode ? t('editProfile') : t('newProfileTitle')}
472
+ </SheetTitle>
473
+ <SheetDescription>
474
+ {editMode
475
+ ? t('editProfileDescription')
476
+ : t('newProfileDescription')}
477
+ </SheetDescription>
478
+ </SheetHeader>
479
+
480
+ <form onSubmit={handleSubmit} className="space-y-6 px-3 pb-8 sm:px-4">
481
+ <div className="grid gap-6 md:grid-cols-2">
482
+ <div className="space-y-2">
483
+ <Label htmlFor="name">{t('nameLabel')}</Label>
484
+ <Input
485
+ id="name"
486
+ value={formData.name}
487
+ onChange={(event) =>
488
+ updateFormField('name', event.target.value)
489
+ }
490
+ placeholder={t('namePlaceholder')}
491
+ required
492
+ />
493
+ </div>
494
+
495
+ <div className="space-y-2">
496
+ <Label htmlFor="slug">{t('slugLabel')}</Label>
497
+ <Input
498
+ id="slug"
499
+ value={formData.slug}
500
+ onChange={(event) =>
501
+ updateFormField('slug', event.target.value)
502
+ }
503
+ placeholder={t('slugPlaceholder')}
504
+ required
505
+ />
506
+ </div>
507
+
508
+ <div className="space-y-2">
509
+ <Label htmlFor="provider">{t('providerLabel')}</Label>
510
+ <Select
511
+ value={formData.provider}
512
+ onValueChange={handleProviderChange}
513
+ >
514
+ <SelectTrigger className="w-full" id="provider">
515
+ <SelectValue />
516
+ </SelectTrigger>
517
+ <SelectContent>
518
+ <SelectItem value="SMTP">{t('providerSMTP')}</SelectItem>
519
+ <SelectItem value="GMAIL">{t('providerGmail')}</SelectItem>
520
+ <SelectItem value="SES">{t('providerSES')}</SelectItem>
521
+ </SelectContent>
522
+ </Select>
523
+ </div>
524
+
525
+ <div className="space-y-2">
526
+ <Label htmlFor="from-email">{t('fromEmailLabel')}</Label>
527
+ <Input
528
+ id="from-email"
529
+ type="email"
530
+ value={formData.from_email}
531
+ onChange={(event) =>
532
+ updateFormField('from_email', event.target.value)
533
+ }
534
+ placeholder={t('fromEmailPlaceholder')}
535
+ required
536
+ />
537
+ </div>
538
+
539
+ <div className="space-y-2">
540
+ <Label htmlFor="from-name">{t('fromNameLabel')}</Label>
541
+ <Input
542
+ id="from-name"
543
+ value={formData.from_name}
544
+ onChange={(event) =>
545
+ updateFormField('from_name', event.target.value)
546
+ }
547
+ placeholder={t('fromNamePlaceholder')}
548
+ />
549
+ </div>
550
+
551
+ <div className="space-y-2">
552
+ <Label htmlFor="reply-to-email">{t('replyToEmailLabel')}</Label>
553
+ <Input
554
+ id="reply-to-email"
555
+ type="email"
556
+ value={formData.reply_to_email}
557
+ onChange={(event) =>
558
+ updateFormField('reply_to_email', event.target.value)
559
+ }
560
+ placeholder={t('replyToEmailPlaceholder')}
561
+ />
562
+ </div>
563
+
564
+ <div className="space-y-2">
565
+ <Label htmlFor="reply-to-name">{t('replyToNameLabel')}</Label>
566
+ <Input
567
+ id="reply-to-name"
568
+ value={formData.reply_to_name}
569
+ onChange={(event) =>
570
+ updateFormField('reply_to_name', event.target.value)
571
+ }
572
+ placeholder={t('replyToNamePlaceholder')}
573
+ />
574
+ </div>
575
+ </div>
576
+
577
+ {formData.provider === 'SMTP' ? (
578
+ <div className="space-y-4 rounded-lg border p-4">
579
+ <div className="flex items-center gap-2">
580
+ <Mail className="h-4 w-4" />
581
+ <h3 className="text-sm font-semibold">
582
+ {t('smtpSectionTitle')}
583
+ </h3>
584
+ </div>
585
+ <div className="grid gap-6 md:grid-cols-2">
586
+ <div className="space-y-2">
587
+ <Label htmlFor="smtp-host">{t('smtpHostLabel')}</Label>
588
+ <Input
589
+ id="smtp-host"
590
+ value={formData.smtp_host}
591
+ onChange={(event) =>
592
+ updateFormField('smtp_host', event.target.value)
593
+ }
594
+ placeholder={t('smtpHostPlaceholder')}
595
+ required
596
+ />
597
+ </div>
598
+ <div className="space-y-2">
599
+ <Label htmlFor="smtp-port">{t('smtpPortLabel')}</Label>
600
+ <Input
601
+ id="smtp-port"
602
+ type="number"
603
+ min="1"
604
+ value={formData.smtp_port}
605
+ onChange={(event) =>
606
+ updateFormField('smtp_port', event.target.value)
607
+ }
608
+ placeholder={t('smtpPortPlaceholder')}
609
+ required
610
+ />
611
+ </div>
612
+ <div className="space-y-2">
613
+ <Label htmlFor="smtp-username">
614
+ {t('smtpUsernameLabel')}
615
+ </Label>
616
+ <Input
617
+ id="smtp-username"
618
+ value={formData.smtp_username}
619
+ onChange={(event) =>
620
+ updateFormField('smtp_username', event.target.value)
621
+ }
622
+ placeholder={t('smtpUsernamePlaceholder')}
623
+ required
624
+ />
625
+ </div>
626
+ <div className="space-y-2">
627
+ <Label htmlFor="smtp-password">
628
+ {t('smtpPasswordLabel')}
629
+ </Label>
630
+ <Input
631
+ id="smtp-password"
632
+ type="password"
633
+ value={formData.smtp_password}
634
+ onChange={(event) =>
635
+ updateFormField('smtp_password', event.target.value)
636
+ }
637
+ placeholder={t('smtpPasswordPlaceholder')}
638
+ required
639
+ />
640
+ </div>
641
+ </div>
642
+ <div className="flex items-center justify-between rounded-md border px-4 py-3">
643
+ <div className="space-y-1">
644
+ <p className="text-sm font-medium">
645
+ {t('smtpSecureLabel')}
646
+ </p>
647
+ <p className="text-xs text-muted-foreground">
648
+ {t('smtpSecureHelp')}
649
+ </p>
650
+ </div>
651
+ <Switch
652
+ checked={formData.smtp_secure}
653
+ onCheckedChange={(checked) =>
654
+ updateFormField('smtp_secure', checked)
655
+ }
656
+ />
657
+ </div>
658
+ </div>
659
+ ) : null}
660
+
661
+ {formData.provider === 'GMAIL' ? (
662
+ <div className="space-y-4 rounded-lg border p-4">
663
+ <div className="flex items-center gap-2">
664
+ <Mail className="h-4 w-4" />
665
+ <h3 className="text-sm font-semibold">
666
+ {t('gmailSectionTitle')}
667
+ </h3>
668
+ </div>
669
+ <div className="grid gap-6 md:grid-cols-2">
670
+ <div className="space-y-2">
671
+ <Label htmlFor="gmail-client-id">
672
+ {t('gmailClientIdLabel')}
673
+ </Label>
674
+ <Input
675
+ id="gmail-client-id"
676
+ value={formData.gmail_client_id}
677
+ onChange={(event) =>
678
+ updateFormField('gmail_client_id', event.target.value)
679
+ }
680
+ placeholder={t('gmailClientIdPlaceholder')}
681
+ required
682
+ />
683
+ </div>
684
+ <div className="space-y-2">
685
+ <Label htmlFor="gmail-client-secret">
686
+ {t('gmailClientSecretLabel')}
687
+ </Label>
688
+ <Input
689
+ id="gmail-client-secret"
690
+ type="password"
691
+ value={formData.gmail_client_secret}
692
+ onChange={(event) =>
693
+ updateFormField(
694
+ 'gmail_client_secret',
695
+ event.target.value
696
+ )
697
+ }
698
+ placeholder={t('gmailClientSecretPlaceholder')}
699
+ required
700
+ />
701
+ </div>
702
+ <div className="space-y-2 md:col-span-2">
703
+ <Label htmlFor="gmail-refresh-token">
704
+ {t('gmailRefreshTokenLabel')}
705
+ </Label>
706
+ <Input
707
+ id="gmail-refresh-token"
708
+ type="password"
709
+ value={formData.gmail_refresh_token}
710
+ onChange={(event) =>
711
+ updateFormField(
712
+ 'gmail_refresh_token',
713
+ event.target.value
714
+ )
715
+ }
716
+ placeholder={t('gmailRefreshTokenPlaceholder')}
717
+ required
718
+ />
719
+ </div>
720
+ </div>
721
+ </div>
722
+ ) : null}
723
+
724
+ {formData.provider === 'SES' ? (
725
+ <div className="space-y-4 rounded-lg border p-4">
726
+ <div className="flex items-center gap-2">
727
+ <Mail className="h-4 w-4" />
728
+ <h3 className="text-sm font-semibold">
729
+ {t('sesSectionTitle')}
730
+ </h3>
731
+ </div>
732
+ <div className="grid gap-6 md:grid-cols-2">
733
+ <div className="space-y-2">
734
+ <Label htmlFor="ses-access-key-id">
735
+ {t('sesAccessKeyIdLabel')}
736
+ </Label>
737
+ <Input
738
+ id="ses-access-key-id"
739
+ value={formData.ses_access_key_id}
740
+ onChange={(event) =>
741
+ updateFormField('ses_access_key_id', event.target.value)
742
+ }
743
+ placeholder={t('sesAccessKeyIdPlaceholder')}
744
+ required
745
+ />
746
+ </div>
747
+ <div className="space-y-2">
748
+ <Label htmlFor="ses-secret-access-key">
749
+ {t('sesSecretAccessKeyLabel')}
750
+ </Label>
751
+ <Input
752
+ id="ses-secret-access-key"
753
+ type="password"
754
+ value={formData.ses_secret_access_key}
755
+ onChange={(event) =>
756
+ updateFormField(
757
+ 'ses_secret_access_key',
758
+ event.target.value
759
+ )
760
+ }
761
+ placeholder={t('sesSecretAccessKeyPlaceholder')}
762
+ required
763
+ />
764
+ </div>
765
+ <div className="space-y-2">
766
+ <Label htmlFor="ses-region">{t('sesRegionLabel')}</Label>
767
+ <Input
768
+ id="ses-region"
769
+ value={formData.ses_region}
770
+ onChange={(event) =>
771
+ updateFormField('ses_region', event.target.value)
772
+ }
773
+ placeholder={t('sesRegionPlaceholder')}
774
+ required
775
+ />
776
+ </div>
777
+ </div>
778
+ </div>
779
+ ) : null}
780
+
781
+ <div className="flex justify-end border-t pt-4">
782
+ {canSubmitProfile ? (
783
+ <Button type="submit" className="min-w-40">
784
+ {editMode ? t('saveChanges') : t('createProfile')}
785
+ </Button>
786
+ ) : (
787
+ <Button
788
+ type="button"
789
+ className="min-w-40"
790
+ onClick={handleTest}
791
+ disabled={isTesting}
792
+ >
793
+ {isTesting ? t('testing') : t('testProfile')}
794
+ </Button>
795
+ )}
796
+ </div>
797
+ </form>
798
+ </SheetContent>
799
+ </Sheet>
800
+
801
+ <AlertDialog
802
+ open={isDeleteDialogOpen}
803
+ onOpenChange={setIsDeleteDialogOpen}
804
+ >
805
+ <AlertDialogContent>
806
+ <AlertDialogHeader>
807
+ <AlertDialogTitle>{t('deleteConfirmTitle')}</AlertDialogTitle>
808
+ <AlertDialogDescription>
809
+ {t('deleteConfirmDescription')}
810
+ </AlertDialogDescription>
811
+ </AlertDialogHeader>
812
+ <AlertDialogFooter>
813
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
814
+ <AlertDialogAction
815
+ onClick={confirmDelete}
816
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
817
+ >
818
+ {t('delete')}
819
+ </AlertDialogAction>
820
+ </AlertDialogFooter>
821
+ </AlertDialogContent>
822
+ </AlertDialog>
823
+ </Page>
824
+ );
825
+ }