Skip to content

Commit 0ded959

Browse files
committed
system/self-test-emails
1 parent 5b46545 commit 0ded959

3 files changed

Lines changed: 231 additions & 0 deletions

File tree

apps/api-manager/src/lib/config/navigation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
Zap,
4444
ShieldOff,
4545
MessageSquare,
46+
Mail,
4647
} from "@lucide/svelte";
4748
import { env } from "$env/dynamic/public";
4849

@@ -144,6 +145,11 @@ function buildSystemItems(): NavigationItem[] {
144145
label: "Migrations",
145146
iconComponent: GitBranch,
146147
},
148+
{
149+
href: "/system/self-test-emails",
150+
label: "Self Test Email",
151+
iconComponent: Mail,
152+
},
147153
{
148154
href: "/system/webui-props",
149155
label: "WebUI Props",

apps/api-manager/src/lib/utils/roleChecker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
126126
"/system/webui-props/[id]/delete": {
127127
required: [{ role: "CanDeleteWebUiProps" }],
128128
},
129+
"/system/self-test-emails": {
130+
required: [{ role: "CanCreateTestEmail" }],
131+
},
129132
"/system/signal-channels": {
130133
required: [],
131134
},
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<script lang="ts">
2+
interface TestEmailResult {
3+
to: string;
4+
from: string;
5+
subject: string;
6+
message_id: string;
7+
}
8+
9+
let isSending = $state(false);
10+
let error = $state<string | null>(null);
11+
let result = $state<TestEmailResult | null>(null);
12+
13+
async function sendTestEmail() {
14+
try {
15+
isSending = true;
16+
error = null;
17+
result = null;
18+
19+
const response = await fetch(
20+
"/proxy/obp/v7.0.0/management/self-test-emails",
21+
{
22+
method: "POST",
23+
headers: { "Content-Type": "application/json" },
24+
body: "{}",
25+
},
26+
);
27+
28+
const data = await response.json().catch(() => ({}));
29+
30+
if (!response.ok) {
31+
throw new Error(data.message || `HTTP ${response.status}`);
32+
}
33+
34+
result = data as TestEmailResult;
35+
} catch (err) {
36+
error = err instanceof Error ? err.message : "Failed to send test email";
37+
} finally {
38+
isSending = false;
39+
}
40+
}
41+
</script>
42+
43+
<svelte:head>
44+
<title>Send Self Test Email - API Manager II</title>
45+
</svelte:head>
46+
47+
<div class="container mx-auto px-4 py-8">
48+
<div class="panel">
49+
<div class="panel-header">
50+
<h1 class="panel-title">Send Self Test Email</h1>
51+
<p class="panel-subtitle">
52+
Send a test email to your own address to verify SMTP delivery from this OBP server.
53+
</p>
54+
</div>
55+
56+
<div class="panel-content">
57+
<p class="explainer">
58+
The recipient is always your authenticated user's email address. There are no
59+
parameters to set. On success, the API returns the recipient, sender, subject
60+
and the message-id assigned by the SMTP server.
61+
</p>
62+
63+
{#if error}
64+
<div class="alert alert-error" data-testid="test-email-error">{error}</div>
65+
{/if}
66+
67+
{#if result}
68+
<div class="alert alert-success" data-testid="test-email-success">
69+
<div class="result-line"><span class="result-label">To:</span> <span data-testid="result-to">{result.to}</span></div>
70+
<div class="result-line"><span class="result-label">From:</span> <span data-testid="result-from">{result.from}</span></div>
71+
<div class="result-line"><span class="result-label">Subject:</span> <span data-testid="result-subject">{result.subject}</span></div>
72+
<div class="result-line"><span class="result-label">Message ID:</span> <span data-testid="result-message-id">{result.message_id}</span></div>
73+
</div>
74+
{/if}
75+
76+
<div class="form-actions">
77+
<button
78+
class="btn-send"
79+
name="send-test-email"
80+
data-testid="send-test-email-button"
81+
onclick={sendTestEmail}
82+
disabled={isSending}
83+
>
84+
{isSending ? "Sending..." : "Send Test Email"}
85+
</button>
86+
</div>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<style>
92+
.container {
93+
max-width: 800px;
94+
}
95+
96+
.panel {
97+
background: white;
98+
border-radius: 8px;
99+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
100+
overflow: hidden;
101+
}
102+
103+
:global([data-mode="dark"]) .panel {
104+
background: rgb(var(--color-surface-800));
105+
}
106+
107+
.panel-header {
108+
padding: 1.25rem 1.5rem;
109+
border-bottom: 1px solid #e5e7eb;
110+
}
111+
112+
:global([data-mode="dark"]) .panel-header {
113+
border-bottom-color: rgb(var(--color-surface-700));
114+
}
115+
116+
.panel-title {
117+
font-size: 1.25rem;
118+
font-weight: 600;
119+
color: #111827;
120+
margin: 0;
121+
}
122+
123+
:global([data-mode="dark"]) .panel-title {
124+
color: var(--color-surface-100);
125+
}
126+
127+
.panel-subtitle {
128+
font-size: 0.8125rem;
129+
color: #6b7280;
130+
margin: 0.25rem 0 0;
131+
}
132+
133+
:global([data-mode="dark"]) .panel-subtitle {
134+
color: var(--color-surface-400);
135+
}
136+
137+
.panel-content {
138+
padding: 1.5rem;
139+
display: flex;
140+
flex-direction: column;
141+
gap: 1rem;
142+
}
143+
144+
.explainer {
145+
font-size: 0.875rem;
146+
color: #374151;
147+
margin: 0;
148+
line-height: 1.5;
149+
}
150+
151+
:global([data-mode="dark"]) .explainer {
152+
color: var(--color-surface-300);
153+
}
154+
155+
.form-actions {
156+
padding-top: 0.25rem;
157+
}
158+
159+
.btn-send {
160+
padding: 0.5rem 1.5rem;
161+
background: #51b265;
162+
color: white;
163+
border: none;
164+
border-radius: 6px;
165+
font-size: 0.875rem;
166+
font-weight: 600;
167+
cursor: pointer;
168+
transition: background-color 0.2s;
169+
}
170+
171+
.btn-send:hover:not(:disabled) {
172+
background: #3d9e52;
173+
}
174+
175+
.btn-send:disabled {
176+
opacity: 0.6;
177+
cursor: not-allowed;
178+
}
179+
180+
.alert {
181+
padding: 0.75rem 1rem;
182+
border-radius: 6px;
183+
font-size: 0.875rem;
184+
}
185+
186+
.alert-error {
187+
background: #fee2e2;
188+
color: #991b1b;
189+
border: 1px solid #fecaca;
190+
}
191+
192+
:global([data-mode="dark"]) .alert-error {
193+
background: rgb(var(--color-error-900));
194+
color: rgb(var(--color-error-200));
195+
border-color: rgb(var(--color-error-800));
196+
}
197+
198+
.alert-success {
199+
background: #d1fae5;
200+
color: #065f46;
201+
border: 1px solid #a7f3d0;
202+
display: flex;
203+
flex-direction: column;
204+
gap: 0.25rem;
205+
}
206+
207+
:global([data-mode="dark"]) .alert-success {
208+
background: rgb(var(--color-success-900));
209+
color: rgb(var(--color-success-200));
210+
border-color: rgb(var(--color-success-800));
211+
}
212+
213+
.result-line {
214+
font-family: monospace;
215+
font-size: 0.8125rem;
216+
}
217+
218+
.result-label {
219+
font-weight: 600;
220+
margin-right: 0.25rem;
221+
}
222+
</style>

0 commit comments

Comments
 (0)