Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions api/routes/v1/inboxes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@ router.get(
router.post(
"/",
body("name").isString().notEmpty().trim(),
body("domain").optional().isString().notEmpty().trim(),
body("email").optional().isString().notEmpty().trim(),
expressValidatorMiddleware,
async (
req: Request<
{ organizationId: string },
{},
{ name: string; domain?: string }
{ name: string; email?: string }
>,
res: Response
) => {
let domainId: string | undefined;
if (req.body.domain) {
if (req.body.email) {
const domainName = req.body.email?.split("@")[1];
if (!domainName) {
return res.status(400).json({ error: "Invalid email address" });
}
const domain = await getVerifiedDomainByOrganizationIdAndName({
organizationId: req.organization._id.toString(),
name: req.body.domain,
name: domainName,
});
if (!domain) {
return res.status(404).json({ error: "Domain not found" });
Expand All @@ -56,6 +60,7 @@ router.post(
organization_id: req.organization._id.toString(),
name: req.body.name,
domain_id: domainId,
email: domainId && req.body.email,
});

await sendWebhookEvent({
Expand Down
93 changes: 93 additions & 0 deletions app/app/pages/inboxes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
<p class="inbox-address">
{{ inbox.email ?? inbox.address ?? 'Generated on first reply' }}
</p>
<div class="key-secret">
<span class="secret-label">ID</span>
<span class="secret-value">{{ inbox._id ?? '••••••••••••••••' }}</span>
<button type="button" class="button-copy" aria-label="Copy API key" @click="copyInboxId(inbox)">
<span v-if="copiedInboxId === (inbox._id ?? inbox.email)">Copied</span>
<span v-else>Copy</span>
</button>
</div>
Comment on lines +46 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Copy‑ID UX is solid; fix “API key” wording and tighten copied state check

The clipboard implementation with guards and a 2s reset is clean. A couple of small inconsistencies:

  • The button aria-label and the error message say “API key”, but the UI is clearly copying the inbox ID.
  • copiedInboxId is only ever set from inbox._id, so comparing against (inbox._id ?? inbox.email) is unnecessary and slightly confusing.

You can address both with a small tweak:

-          <button type="button" class="button-copy" aria-label="Copy API key" @click="copyInboxId(inbox)">
-            <span v-if="copiedInboxId === (inbox._id ?? inbox.email)">Copied</span>
+          <button type="button" class="button-copy" aria-label="Copy inbox ID" @click="copyInboxId(inbox)">
+            <span v-if="copiedInboxId === inbox._id">Copied</span>
             <span v-else>Copy</span>
           </button>
-  } catch (error) {
-    console.error('Failed to copy API key', error);
+  } catch (error) {
+    console.error('Failed to copy inbox ID', error);
   }

Also applies to: 171-207

🤖 Prompt for AI Agents
In app/app/pages/inboxes.vue around lines 46 to 53 (and similarly at lines
171–207), the button aria-label and error text incorrectly reference "API key"
while the UI copies an inbox ID, and the copied state comparison mixes fallback
email causing unnecessary complexity; change the aria-label and any error text
to "Copy inbox ID" (or similar consistent wording), and simplify the copied
state check to compare only against inbox._id (remove the (inbox._id ??
inbox.email) fallback) and ensure copiedInboxId is set/reset consistently with
that value.

<footer>
<span>Created {{ formatDate(inbox.createdAt) }}</span>
<span v-if="inbox.status" class="status">
Expand Down Expand Up @@ -160,6 +168,8 @@ const form = reactive({
displayName: '',
email: ''
});
const copiedInboxId = ref<string | null>(null);
let copyTimeout: ReturnType<typeof setTimeout> | null = null;

const formatDate = (value?: string) => {
if (!value) {
Expand All @@ -176,6 +186,32 @@ const formatDate = (value?: string) => {
});
};

const copyInboxId = async (inbox: Inbox) => {
if (!inbox._id || typeof navigator === 'undefined' || !navigator.clipboard) {
return;
}

try {
await navigator.clipboard.writeText(inbox._id);
copiedInboxId.value = inbox._id ?? null;
if (copyTimeout) {
clearTimeout(copyTimeout);
}
copyTimeout = setTimeout(() => {
copiedInboxId.value = null;
copyTimeout = null;
}, 2000);
} catch (error) {
console.error('Failed to copy API key', error);
}
};

onBeforeUnmount(() => {
if (copyTimeout) {
clearTimeout(copyTimeout);
}
});

const loadInboxes = async () => {
const organizationId = session.organizationId.value;
const token = session.token.value;
Expand Down Expand Up @@ -367,6 +403,18 @@ watch(
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
max-width: calc(3 * 1fr + 3 * 1.5rem);
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1100px) {
.inboxes-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 700px) {
.inboxes-grid {
grid-template-columns: 1fr;
}
}

.inbox-card {
Expand Down Expand Up @@ -597,6 +645,51 @@ watch(
opacity: 0;
}

.key-secret {
display: flex;
align-items: center;
gap: 0.75rem;
background: rgba(15, 15, 25, 0.75);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 0.9rem;
padding: 0.75rem 1rem;
max-width: 300px;
}

.secret-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.55);
text-transform: uppercase;
letter-spacing: 0.08em;
}

.secret-value {
flex: 1;
font-family: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.button-copy {
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.85);
border-radius: 0.75rem;
padding: 0.45rem 0.9rem;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}

.button-copy:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}

@media (max-width: 780px) {
.page-heading {
flex-direction: column;
Expand Down
21 changes: 18 additions & 3 deletions node-sdk/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import axios from "axios";
import type {
ApiKeyMethods,
DomainMethods,
InboxMethods,
CreateApiKeyParams,
GetApiKeyParams,
DeleteApiKeyParams,
CreateDomainParams,
GetDomainParams,
VerifyDomainParams,
DeleteDomainParams,
CreateInboxParams,
SendMessageParams,
ReplyMessageParams,
} from "./types/sendook-api";

class SendookAPI {
private apiSecret: string;
Expand All @@ -9,7 +24,7 @@ class SendookAPI {
this.apiUrl = apiUrl || "https://api.sendook.com";
}

public apiKey = {
public apiKey: ApiKeyMethods = {
create: async ({
name,
}: {
Expand Down Expand Up @@ -58,7 +73,7 @@ class SendookAPI {
},
};

public domain = {
public domain: DomainMethods = {
create: async ({
name,
}: {
Expand Down Expand Up @@ -111,7 +126,7 @@ class SendookAPI {
},
};

public inbox = {
public inbox: InboxMethods = {
create: async ({
name,
email,
Expand Down
22 changes: 20 additions & 2 deletions node-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
{
"name": "@sendook/node",
"version": "0.2.0",
"module": "index.ts",
"version": "0.4.0",
"type": "module",
"files": [
"dist"
],
"module": "./dist/index.js",
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
Expand Down
90 changes: 90 additions & 0 deletions node-sdk/types/sendook-api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Type definitions for SendookAPI
*/

export interface CreateApiKeyParams {
name: string;
}

export interface GetApiKeyParams {
apiKeyId: string;
}

export interface DeleteApiKeyParams {
apiKeyId: string;
}

export interface CreateDomainParams {
name: string;
}

export interface GetDomainParams {
domainId: string;
}

export interface VerifyDomainParams {
domainId: string;
}

export interface DeleteDomainParams {
domainId: string;
}

export interface CreateInboxParams {
name: string;
email?: string;
}

export interface SendMessageParams {
inboxId: string;
to: string[];
cc?: string[];
bcc?: string[];
labels?: string[];
subject: string;
text: string;
html: string;
}

export interface ReplyMessageParams {
inboxId: string;
messageId: string;
text: string;
html: string;
}

export interface ApiKeyMethods {
create: (params: CreateApiKeyParams) => Promise<any>;
list: () => Promise<any>;
get: (params: GetApiKeyParams) => Promise<any>;
delete: (params: DeleteApiKeyParams) => Promise<any>;
}

export interface DomainMethods {
create: (params: CreateDomainParams) => Promise<any>;
get: (params: GetDomainParams) => Promise<any>;
verify: (params: VerifyDomainParams) => Promise<any>;
delete: (params: DeleteDomainParams) => Promise<any>;
}

export interface MessageMethods {
send: (params: SendMessageParams) => Promise<any>;
reply: (params: ReplyMessageParams) => Promise<any>;
list: (inboxId: string, query?: string) => Promise<any>;
get: (inboxId: string, messageId: string) => Promise<any>;
}

export interface ThreadMethods {
list: (inboxId: string) => Promise<any>;
get: (inboxId: string, threadId: string) => Promise<any>;
}

export interface InboxMethods {
create: (params: CreateInboxParams) => Promise<any>;
list: () => Promise<any>;
get: (inboxId: string) => Promise<any>;
delete: (inboxId: string) => Promise<any>;
message: MessageMethods;
thread: ThreadMethods;
}