@hed-hog/core 0.0.321 → 0.0.322

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 (64) 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/core.module.d.ts.map +1 -1
  4. package/dist/core.module.js +3 -0
  5. package/dist/core.module.js.map +1 -1
  6. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  7. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  8. package/dist/file/file.controller.d.ts +2 -2
  9. package/dist/file/file.service.d.ts +4 -4
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/mail-profile/dto/create.dto.d.ts +23 -0
  15. package/dist/mail-profile/dto/create.dto.d.ts.map +1 -0
  16. package/dist/mail-profile/dto/create.dto.js +139 -0
  17. package/dist/mail-profile/dto/create.dto.js.map +1 -0
  18. package/dist/mail-profile/dto/update.dto.d.ts +6 -0
  19. package/dist/mail-profile/dto/update.dto.d.ts.map +1 -0
  20. package/dist/mail-profile/dto/update.dto.js +9 -0
  21. package/dist/mail-profile/dto/update.dto.js.map +1 -0
  22. package/dist/mail-profile/mail-profile.controller.d.ts +94 -0
  23. package/dist/mail-profile/mail-profile.controller.d.ts.map +1 -0
  24. package/dist/mail-profile/mail-profile.controller.js +95 -0
  25. package/dist/mail-profile/mail-profile.controller.js.map +1 -0
  26. package/dist/mail-profile/mail-profile.module.d.ts +3 -0
  27. package/dist/mail-profile/mail-profile.module.d.ts.map +1 -0
  28. package/dist/mail-profile/mail-profile.module.js +31 -0
  29. package/dist/mail-profile/mail-profile.module.js.map +1 -0
  30. package/dist/mail-profile/mail-profile.service.d.ts +109 -0
  31. package/dist/mail-profile/mail-profile.service.d.ts.map +1 -0
  32. package/dist/mail-profile/mail-profile.service.js +278 -0
  33. package/dist/mail-profile/mail-profile.service.js.map +1 -0
  34. package/dist/session/session.controller.d.ts +1 -1
  35. package/dist/session/session.service.d.ts +3 -3
  36. package/dist/user/user.controller.d.ts +18 -3
  37. package/dist/user/user.controller.d.ts.map +1 -1
  38. package/dist/user/user.controller.js +22 -0
  39. package/dist/user/user.controller.js.map +1 -1
  40. package/dist/user/user.service.d.ts +22 -7
  41. package/dist/user/user.service.d.ts.map +1 -1
  42. package/dist/user/user.service.js +99 -0
  43. package/dist/user/user.service.js.map +1 -1
  44. package/hedhog/data/menu.yaml +35 -3
  45. package/hedhog/data/route.yaml +64 -0
  46. package/hedhog/frontend/app/mail/page.tsx.ejs +5 -0
  47. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +828 -0
  48. package/hedhog/frontend/app/users/page.tsx.ejs +25 -1383
  49. package/hedhog/frontend/app/users/permissions.tsx.ejs +109 -55
  50. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +304 -0
  51. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +1385 -0
  52. package/hedhog/frontend/messages/en.json +91 -0
  53. package/hedhog/frontend/messages/pt.json +91 -0
  54. package/hedhog/table/mail_profile.yaml +75 -0
  55. package/package.json +4 -4
  56. package/src/core.module.ts +3 -0
  57. package/src/index.ts +2 -0
  58. package/src/mail-profile/dto/create.dto.ts +145 -0
  59. package/src/mail-profile/dto/update.dto.ts +4 -0
  60. package/src/mail-profile/mail-profile.controller.ts +56 -0
  61. package/src/mail-profile/mail-profile.module.ts +18 -0
  62. package/src/mail-profile/mail-profile.service.ts +308 -0
  63. package/src/user/user.controller.ts +13 -0
  64. package/src/user/user.service.ts +98 -5
@@ -0,0 +1,828 @@
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, useMemo, 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 = useMemo(
267
+ () => JSON.stringify(createPayload()),
268
+ [formData]
269
+ );
270
+
271
+ const canCreateProfile = editMode || testedSignature === currentSignature;
272
+
273
+ const handleTest = async () => {
274
+ const payload = createPayload();
275
+
276
+ try {
277
+ setIsTesting(true);
278
+ const { data } = await request<{ success: boolean; destination: string }>({
279
+ url: '/mail-profile/test',
280
+ method: 'POST',
281
+ data: payload,
282
+ });
283
+
284
+ setTestedSignature(JSON.stringify(payload));
285
+ toast.success(
286
+ t('testSuccess', {
287
+ email: data.destination || payload.from_email,
288
+ })
289
+ );
290
+ } catch (error) {
291
+ console.error('Error testing mail profile:', error);
292
+ setTestedSignature(null);
293
+ toast.error(t('testError'));
294
+ } finally {
295
+ setIsTesting(false);
296
+ }
297
+ };
298
+
299
+ const handleSubmit = async (event: React.FormEvent) => {
300
+ event.preventDefault();
301
+
302
+ const payload = createPayload();
303
+
304
+ try {
305
+ if (editMode && selectedProfile) {
306
+ await request({
307
+ url: `/mail-profile/${selectedProfile.id}`,
308
+ method: 'PATCH',
309
+ data: payload,
310
+ });
311
+ toast.success(t('updateSuccess'));
312
+ } else {
313
+ await request({
314
+ url: '/mail-profile',
315
+ method: 'POST',
316
+ data: payload,
317
+ });
318
+ toast.success(t('createSuccess'));
319
+ }
320
+
321
+ setIsDialogOpen(false);
322
+ setTestedSignature(null);
323
+ refetch();
324
+ } catch (error) {
325
+ console.error('Error saving mail profile:', error);
326
+ toast.error(editMode ? t('updateError') : t('createError'));
327
+ }
328
+ };
329
+
330
+ const renderSender = (profile: MailProfile) => {
331
+ if (profile.from_name) {
332
+ return `${profile.from_name} <${profile.from_email}>`;
333
+ }
334
+
335
+ return profile.from_email;
336
+ };
337
+
338
+ const renderReplyTo = (profile: MailProfile) => {
339
+ if (!profile.reply_to_email) {
340
+ return t('notConfigured');
341
+ }
342
+
343
+ if (profile.reply_to_name) {
344
+ return `${profile.reply_to_name} <${profile.reply_to_email}>`;
345
+ }
346
+
347
+ return profile.reply_to_email;
348
+ };
349
+
350
+ return (
351
+ <Page>
352
+ <PageHeader
353
+ breadcrumbs={[
354
+ { label: t('breadcrumbHome'), href: '/' },
355
+ { label: t('breadcrumbTitle') },
356
+ ]}
357
+ actions={[
358
+ {
359
+ label: t('newProfile'),
360
+ onClick: handleCreate,
361
+ variant: 'default' as const,
362
+ },
363
+ ]}
364
+ title={t('title')}
365
+ description={t('description')}
366
+ />
367
+
368
+ <SearchBar
369
+ searchQuery={search}
370
+ onSearchChange={(value) => {
371
+ setSearch(value);
372
+ setPage(1);
373
+ }}
374
+ onSearch={() => setPage(1)}
375
+ placeholder={t('searchPlaceholder')}
376
+ />
377
+
378
+ {profiles.length === 0 ? (
379
+ <EmptyState
380
+ icon={<Send className="h-12 w-12" />}
381
+ title={t('noProfilesFound')}
382
+ description={t('noProfilesHint')}
383
+ actionLabel={t('newProfile')}
384
+ onAction={handleCreate}
385
+ />
386
+ ) : (
387
+ <div className="rounded-md border">
388
+ <Table>
389
+ <TableHeader>
390
+ <TableRow>
391
+ <TableHead>{t('tableName')}</TableHead>
392
+ <TableHead>{t('tableSlug')}</TableHead>
393
+ <TableHead>{t('tableProvider')}</TableHead>
394
+ <TableHead>{t('tableSender')}</TableHead>
395
+ <TableHead>{t('tableReplyTo')}</TableHead>
396
+ <TableHead>{t('tableUpdated')}</TableHead>
397
+ <TableHead className="text-right">
398
+ {t('tableActions')}
399
+ </TableHead>
400
+ </TableRow>
401
+ </TableHeader>
402
+ <TableBody>
403
+ {profiles.map((profile) => (
404
+ <TableRow
405
+ key={profile.id}
406
+ className="cursor-pointer"
407
+ onDoubleClick={() => handleEdit(profile)}
408
+ >
409
+ <TableCell className="font-medium">{profile.name}</TableCell>
410
+ <TableCell className="font-mono text-sm">
411
+ {profile.slug}
412
+ </TableCell>
413
+ <TableCell>
414
+ <Badge variant="secondary">{profile.provider}</Badge>
415
+ </TableCell>
416
+ <TableCell>{renderSender(profile)}</TableCell>
417
+ <TableCell>{renderReplyTo(profile)}</TableCell>
418
+ <TableCell className="text-muted-foreground">
419
+ {formatDate(
420
+ profile.updated_at,
421
+ getSettingValue,
422
+ currentLocaleCode
423
+ )}
424
+ </TableCell>
425
+ <TableCell className="text-right">
426
+ <div className="flex justify-end gap-2">
427
+ <Button
428
+ variant="ghost"
429
+ size="icon"
430
+ onClick={(event) => {
431
+ event.stopPropagation();
432
+ handleEdit(profile);
433
+ }}
434
+ >
435
+ <Pencil className="h-4 w-4" />
436
+ </Button>
437
+ <Button
438
+ variant="ghost"
439
+ size="icon"
440
+ onClick={(event) => {
441
+ event.stopPropagation();
442
+ handleDelete(profile.id);
443
+ }}
444
+ >
445
+ <Trash2 className="h-4 w-4 text-destructive" />
446
+ </Button>
447
+ </div>
448
+ </TableCell>
449
+ </TableRow>
450
+ ))}
451
+ </TableBody>
452
+ </Table>
453
+ </div>
454
+ )}
455
+
456
+ <PaginationFooter
457
+ currentPage={page}
458
+ pageSize={pageSize}
459
+ totalItems={totalItems}
460
+ onPageChange={setPage}
461
+ onPageSizeChange={(newSize) => {
462
+ setPageSize(newSize);
463
+ setPage(1);
464
+ }}
465
+ />
466
+
467
+ <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
468
+ <SheetContent
469
+ side="right"
470
+ className="w-full max-w-[95vw] overflow-y-auto sm:max-w-4xl"
471
+ >
472
+ <SheetHeader>
473
+ <SheetTitle>
474
+ {editMode ? t('editProfile') : t('newProfileTitle')}
475
+ </SheetTitle>
476
+ <SheetDescription>
477
+ {editMode
478
+ ? t('editProfileDescription')
479
+ : t('newProfileDescription')}
480
+ </SheetDescription>
481
+ </SheetHeader>
482
+
483
+ <form onSubmit={handleSubmit} className="space-y-6 px-3 pb-8 sm:px-4">
484
+ <div className="grid gap-6 md:grid-cols-2">
485
+ <div className="space-y-2">
486
+ <Label htmlFor="name">{t('nameLabel')}</Label>
487
+ <Input
488
+ id="name"
489
+ value={formData.name}
490
+ onChange={(event) =>
491
+ updateFormField('name', event.target.value)
492
+ }
493
+ placeholder={t('namePlaceholder')}
494
+ required
495
+ />
496
+ </div>
497
+
498
+ <div className="space-y-2">
499
+ <Label htmlFor="slug">{t('slugLabel')}</Label>
500
+ <Input
501
+ id="slug"
502
+ value={formData.slug}
503
+ onChange={(event) =>
504
+ updateFormField('slug', event.target.value)
505
+ }
506
+ placeholder={t('slugPlaceholder')}
507
+ required
508
+ />
509
+ </div>
510
+
511
+ <div className="space-y-2">
512
+ <Label htmlFor="provider">{t('providerLabel')}</Label>
513
+ <Select
514
+ value={formData.provider}
515
+ onValueChange={handleProviderChange}
516
+ >
517
+ <SelectTrigger className="w-full" id="provider">
518
+ <SelectValue />
519
+ </SelectTrigger>
520
+ <SelectContent>
521
+ <SelectItem value="SMTP">{t('providerSMTP')}</SelectItem>
522
+ <SelectItem value="GMAIL">{t('providerGmail')}</SelectItem>
523
+ <SelectItem value="SES">{t('providerSES')}</SelectItem>
524
+ </SelectContent>
525
+ </Select>
526
+ </div>
527
+
528
+ <div className="space-y-2">
529
+ <Label htmlFor="from-email">{t('fromEmailLabel')}</Label>
530
+ <Input
531
+ id="from-email"
532
+ type="email"
533
+ value={formData.from_email}
534
+ onChange={(event) =>
535
+ updateFormField('from_email', event.target.value)
536
+ }
537
+ placeholder={t('fromEmailPlaceholder')}
538
+ required
539
+ />
540
+ </div>
541
+
542
+ <div className="space-y-2">
543
+ <Label htmlFor="from-name">{t('fromNameLabel')}</Label>
544
+ <Input
545
+ id="from-name"
546
+ value={formData.from_name}
547
+ onChange={(event) =>
548
+ updateFormField('from_name', event.target.value)
549
+ }
550
+ placeholder={t('fromNamePlaceholder')}
551
+ />
552
+ </div>
553
+
554
+ <div className="space-y-2">
555
+ <Label htmlFor="reply-to-email">{t('replyToEmailLabel')}</Label>
556
+ <Input
557
+ id="reply-to-email"
558
+ type="email"
559
+ value={formData.reply_to_email}
560
+ onChange={(event) =>
561
+ updateFormField('reply_to_email', event.target.value)
562
+ }
563
+ placeholder={t('replyToEmailPlaceholder')}
564
+ />
565
+ </div>
566
+
567
+ <div className="space-y-2">
568
+ <Label htmlFor="reply-to-name">{t('replyToNameLabel')}</Label>
569
+ <Input
570
+ id="reply-to-name"
571
+ value={formData.reply_to_name}
572
+ onChange={(event) =>
573
+ updateFormField('reply_to_name', event.target.value)
574
+ }
575
+ placeholder={t('replyToNamePlaceholder')}
576
+ />
577
+ </div>
578
+ </div>
579
+
580
+ {formData.provider === 'SMTP' ? (
581
+ <div className="space-y-4 rounded-lg border p-4">
582
+ <div className="flex items-center gap-2">
583
+ <Mail className="h-4 w-4" />
584
+ <h3 className="text-sm font-semibold">
585
+ {t('smtpSectionTitle')}
586
+ </h3>
587
+ </div>
588
+ <div className="grid gap-6 md:grid-cols-2">
589
+ <div className="space-y-2">
590
+ <Label htmlFor="smtp-host">{t('smtpHostLabel')}</Label>
591
+ <Input
592
+ id="smtp-host"
593
+ value={formData.smtp_host}
594
+ onChange={(event) =>
595
+ updateFormField('smtp_host', event.target.value)
596
+ }
597
+ placeholder={t('smtpHostPlaceholder')}
598
+ required
599
+ />
600
+ </div>
601
+ <div className="space-y-2">
602
+ <Label htmlFor="smtp-port">{t('smtpPortLabel')}</Label>
603
+ <Input
604
+ id="smtp-port"
605
+ type="number"
606
+ min="1"
607
+ value={formData.smtp_port}
608
+ onChange={(event) =>
609
+ updateFormField('smtp_port', event.target.value)
610
+ }
611
+ placeholder={t('smtpPortPlaceholder')}
612
+ required
613
+ />
614
+ </div>
615
+ <div className="space-y-2">
616
+ <Label htmlFor="smtp-username">
617
+ {t('smtpUsernameLabel')}
618
+ </Label>
619
+ <Input
620
+ id="smtp-username"
621
+ value={formData.smtp_username}
622
+ onChange={(event) =>
623
+ updateFormField('smtp_username', event.target.value)
624
+ }
625
+ placeholder={t('smtpUsernamePlaceholder')}
626
+ required
627
+ />
628
+ </div>
629
+ <div className="space-y-2">
630
+ <Label htmlFor="smtp-password">
631
+ {t('smtpPasswordLabel')}
632
+ </Label>
633
+ <Input
634
+ id="smtp-password"
635
+ type="password"
636
+ value={formData.smtp_password}
637
+ onChange={(event) =>
638
+ updateFormField('smtp_password', event.target.value)
639
+ }
640
+ placeholder={t('smtpPasswordPlaceholder')}
641
+ required
642
+ />
643
+ </div>
644
+ </div>
645
+ <div className="flex items-center justify-between rounded-md border px-4 py-3">
646
+ <div className="space-y-1">
647
+ <p className="text-sm font-medium">
648
+ {t('smtpSecureLabel')}
649
+ </p>
650
+ <p className="text-xs text-muted-foreground">
651
+ {t('smtpSecureHelp')}
652
+ </p>
653
+ </div>
654
+ <Switch
655
+ checked={formData.smtp_secure}
656
+ onCheckedChange={(checked) =>
657
+ updateFormField('smtp_secure', checked)
658
+ }
659
+ />
660
+ </div>
661
+ </div>
662
+ ) : null}
663
+
664
+ {formData.provider === 'GMAIL' ? (
665
+ <div className="space-y-4 rounded-lg border p-4">
666
+ <div className="flex items-center gap-2">
667
+ <Mail className="h-4 w-4" />
668
+ <h3 className="text-sm font-semibold">
669
+ {t('gmailSectionTitle')}
670
+ </h3>
671
+ </div>
672
+ <div className="grid gap-6 md:grid-cols-2">
673
+ <div className="space-y-2">
674
+ <Label htmlFor="gmail-client-id">
675
+ {t('gmailClientIdLabel')}
676
+ </Label>
677
+ <Input
678
+ id="gmail-client-id"
679
+ value={formData.gmail_client_id}
680
+ onChange={(event) =>
681
+ updateFormField('gmail_client_id', event.target.value)
682
+ }
683
+ placeholder={t('gmailClientIdPlaceholder')}
684
+ required
685
+ />
686
+ </div>
687
+ <div className="space-y-2">
688
+ <Label htmlFor="gmail-client-secret">
689
+ {t('gmailClientSecretLabel')}
690
+ </Label>
691
+ <Input
692
+ id="gmail-client-secret"
693
+ type="password"
694
+ value={formData.gmail_client_secret}
695
+ onChange={(event) =>
696
+ updateFormField(
697
+ 'gmail_client_secret',
698
+ event.target.value
699
+ )
700
+ }
701
+ placeholder={t('gmailClientSecretPlaceholder')}
702
+ required
703
+ />
704
+ </div>
705
+ <div className="space-y-2 md:col-span-2">
706
+ <Label htmlFor="gmail-refresh-token">
707
+ {t('gmailRefreshTokenLabel')}
708
+ </Label>
709
+ <Input
710
+ id="gmail-refresh-token"
711
+ type="password"
712
+ value={formData.gmail_refresh_token}
713
+ onChange={(event) =>
714
+ updateFormField(
715
+ 'gmail_refresh_token',
716
+ event.target.value
717
+ )
718
+ }
719
+ placeholder={t('gmailRefreshTokenPlaceholder')}
720
+ required
721
+ />
722
+ </div>
723
+ </div>
724
+ </div>
725
+ ) : null}
726
+
727
+ {formData.provider === 'SES' ? (
728
+ <div className="space-y-4 rounded-lg border p-4">
729
+ <div className="flex items-center gap-2">
730
+ <Mail className="h-4 w-4" />
731
+ <h3 className="text-sm font-semibold">
732
+ {t('sesSectionTitle')}
733
+ </h3>
734
+ </div>
735
+ <div className="grid gap-6 md:grid-cols-2">
736
+ <div className="space-y-2">
737
+ <Label htmlFor="ses-access-key-id">
738
+ {t('sesAccessKeyIdLabel')}
739
+ </Label>
740
+ <Input
741
+ id="ses-access-key-id"
742
+ value={formData.ses_access_key_id}
743
+ onChange={(event) =>
744
+ updateFormField('ses_access_key_id', event.target.value)
745
+ }
746
+ placeholder={t('sesAccessKeyIdPlaceholder')}
747
+ required
748
+ />
749
+ </div>
750
+ <div className="space-y-2">
751
+ <Label htmlFor="ses-secret-access-key">
752
+ {t('sesSecretAccessKeyLabel')}
753
+ </Label>
754
+ <Input
755
+ id="ses-secret-access-key"
756
+ type="password"
757
+ value={formData.ses_secret_access_key}
758
+ onChange={(event) =>
759
+ updateFormField(
760
+ 'ses_secret_access_key',
761
+ event.target.value
762
+ )
763
+ }
764
+ placeholder={t('sesSecretAccessKeyPlaceholder')}
765
+ required
766
+ />
767
+ </div>
768
+ <div className="space-y-2">
769
+ <Label htmlFor="ses-region">{t('sesRegionLabel')}</Label>
770
+ <Input
771
+ id="ses-region"
772
+ value={formData.ses_region}
773
+ onChange={(event) =>
774
+ updateFormField('ses_region', event.target.value)
775
+ }
776
+ placeholder={t('sesRegionPlaceholder')}
777
+ required
778
+ />
779
+ </div>
780
+ </div>
781
+ </div>
782
+ ) : null}
783
+
784
+ <div className="flex justify-end border-t pt-4">
785
+ {canCreateProfile ? (
786
+ <Button type="submit" className="min-w-40">
787
+ {editMode ? t('saveChanges') : t('createProfile')}
788
+ </Button>
789
+ ) : (
790
+ <Button
791
+ type="button"
792
+ className="min-w-40"
793
+ onClick={handleTest}
794
+ disabled={isTesting}
795
+ >
796
+ {isTesting ? t('testing') : t('testProfile')}
797
+ </Button>
798
+ )}
799
+ </div>
800
+ </form>
801
+ </SheetContent>
802
+ </Sheet>
803
+
804
+ <AlertDialog
805
+ open={isDeleteDialogOpen}
806
+ onOpenChange={setIsDeleteDialogOpen}
807
+ >
808
+ <AlertDialogContent>
809
+ <AlertDialogHeader>
810
+ <AlertDialogTitle>{t('deleteConfirmTitle')}</AlertDialogTitle>
811
+ <AlertDialogDescription>
812
+ {t('deleteConfirmDescription')}
813
+ </AlertDialogDescription>
814
+ </AlertDialogHeader>
815
+ <AlertDialogFooter>
816
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
817
+ <AlertDialogAction
818
+ onClick={confirmDelete}
819
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
820
+ >
821
+ {t('delete')}
822
+ </AlertDialogAction>
823
+ </AlertDialogFooter>
824
+ </AlertDialogContent>
825
+ </AlertDialog>
826
+ </Page>
827
+ );
828
+ }