From 549f5bccb70fd04cdcd39e059f1a9b42852430df Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 28 Jan 2026 12:00:07 +0100 Subject: [PATCH 1/3] Clean up unused `get_domain_by_id` (use `get` instead) --- ...23a4f6bac81c2f279b6ac197e3ec81720af57.json | 81 ------------------- src/handler/dns.rs | 2 +- src/models/domains.rs | 29 ------- 3 files changed, 1 insertion(+), 111 deletions(-) delete mode 100644 .sqlx/query-e8015b31af95d79f02d3435c5de23a4f6bac81c2f279b6ac197e3ec81720af57.json diff --git a/.sqlx/query-e8015b31af95d79f02d3435c5de23a4f6bac81c2f279b6ac197e3ec81720af57.json b/.sqlx/query-e8015b31af95d79f02d3435c5de23a4f6bac81c2f279b6ac197e3ec81720af57.json deleted file mode 100644 index e2e89e9b..00000000 --- a/.sqlx/query-e8015b31af95d79f02d3435c5de23a4f6bac81c2f279b6ac197e3ec81720af57.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT d.id,\n d.domain,\n d.organization_id,\n d.project_id,\n d.dkim_key_type as \"dkim_key_type: DkimKeyType\",\n d.dkim_pkcs8_der,\n d.verification_status,\n d.created_at,\n d.updated_at\n FROM domains d\n LEFT JOIN projects p ON d.project_id = p.id\n WHERE d.id = $2 AND (d.organization_id = $1 OR p.organization_id = $1)\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "domain", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "organization_id", - "type_info": "Uuid" - }, - { - "ordinal": 3, - "name": "project_id", - "type_info": "Uuid" - }, - { - "ordinal": 4, - "name": "dkim_key_type: DkimKeyType", - "type_info": { - "Custom": { - "name": "dkim_key_type", - "kind": { - "Enum": [ - "rsa_sha256", - "ed25519" - ] - } - } - } - }, - { - "ordinal": 5, - "name": "dkim_pkcs8_der", - "type_info": "Bytea" - }, - { - "ordinal": 6, - "name": "verification_status", - "type_info": "Jsonb" - }, - { - "ordinal": 7, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - false - ] - }, - "hash": "e8015b31af95d79f02d3435c5de23a4f6bac81c2f279b6ac197e3ec81720af57" -} diff --git a/src/handler/dns.rs b/src/handler/dns.rs index d1421ee1..05caf9f8 100644 --- a/src/handler/dns.rs +++ b/src/handler/dns.rs @@ -488,7 +488,7 @@ mod test { async fn domain_verification(pool: PgPool) { let domains = DomainRepository::new(pool, DnsResolver::mock("localhost", 1025)); let domain = domains - .get_domain_by_id( + .get( "44729d9f-a7dc-4226-b412-36a7537f5176".parse().unwrap(), "ed28baa5-57f7-413f-8c77-7797ba6a8780".parse().unwrap(), ) diff --git a/src/models/domains.rs b/src/models/domains.rs index c6e1e573..68012caa 100644 --- a/src/models/domains.rs +++ b/src/models/domains.rs @@ -515,35 +515,6 @@ impl DomainRepository { .try_into() } - pub async fn get_domain_by_id( - &self, - org_id: OrganizationId, - domain_id: DomainId, - ) -> Result { - sqlx::query_as!( - PgDomain, - r#" - SELECT d.id, - d.domain, - d.organization_id, - d.project_id, - d.dkim_key_type as "dkim_key_type: DkimKeyType", - d.dkim_pkcs8_der, - d.verification_status, - d.created_at, - d.updated_at - FROM domains d - LEFT JOIN projects p ON d.project_id = p.id - WHERE d.id = $2 AND (d.organization_id = $1 OR p.organization_id = $1) - "#, - *org_id, - *domain_id - ) - .fetch_one(&self.pool) - .await? - .try_into() - } - pub async fn list(&self, org_id: OrganizationId) -> Result, Error> { sqlx::query_as!( PgDomain, From 7eeeaa9d86dc3e0e130b824f4efcaebbe897eead Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 28 Jan 2026 16:27:57 +0100 Subject: [PATCH 2/3] Change email APIs to be organization-wide --- ...82a33d58bf0c345ac61b65c1e278ad5307e6.json} | 5 +- ...b98a3afeaa8a360cc6b975d5169c5624be53.json} | 8 +- ...495fd812a2d61331310c8a7f12fc7e1674d3.json} | 5 +- ...dd4ae73180f2c3c6646298f66a82d398dad0.json} | 5 +- ...240b888faa743875fd376362566efe9a5c58.json} | 5 +- frontend/src/apiMiddleware.ts | 16 ++- .../components/emails/EmailDeleteButton.tsx | 13 +- .../components/emails/EmailRetryButton.tsx | 6 +- frontend/src/hooks/useEmails.ts | 10 +- src/api/api_keys.rs | 30 ++-- src/api/messages.rs | 128 +++++++----------- src/models/message.rs | 76 ++++------- src/models/smtp_credential.rs | 10 +- src/periodically.rs | 4 +- src/smtp/mod.rs | 10 +- src/test.rs | 6 +- 16 files changed, 126 insertions(+), 211 deletions(-) rename .sqlx/{query-41bc2a929838b6becc9b3062d64d763abfc267a7804c3c1766f1ea498937dc4f.json => query-2164e9389cf2fe0a949600f45b9d82a33d58bf0c345ac61b65c1e278ad5307e6.json} (65%) rename .sqlx/{query-627c5920d7e5416c50f4ec71a8ae6ec034612dca650e047d359d4ec7d1a9246c.json => query-2daf4bc3a73109dfbd4602d8a1b5b98a3afeaa8a360cc6b975d5169c5624be53.json} (88%) rename .sqlx/{query-83ff89816add414b2b8cd70ddd5ecc4a0f531ada0c6b222912ab4baab9f1d813.json => query-61f1a536b2f384a880a1a80183eb495fd812a2d61331310c8a7f12fc7e1674d3.json} (92%) rename .sqlx/{query-14a62a3a727307e357841676756ecff016d7f9f1958a20d0f35edac7043e271d.json => query-72d09375994accc07bf4d65dc2c6dd4ae73180f2c3c6646298f66a82d398dad0.json} (66%) rename .sqlx/{query-2b8067e776808e7cda898bfbd9b9e6b2c023ad2ae85ddc84cac5eca371309d57.json => query-99f9ced07e12f98012dfb179ce8c240b888faa743875fd376362566efe9a5c58.json} (80%) diff --git a/.sqlx/query-41bc2a929838b6becc9b3062d64d763abfc267a7804c3c1766f1ea498937dc4f.json b/.sqlx/query-2164e9389cf2fe0a949600f45b9d82a33d58bf0c345ac61b65c1e278ad5307e6.json similarity index 65% rename from .sqlx/query-41bc2a929838b6becc9b3062d64d763abfc267a7804c3c1766f1ea498937dc4f.json rename to .sqlx/query-2164e9389cf2fe0a949600f45b9d82a33d58bf0c345ac61b65c1e278ad5307e6.json index 22945c2a..2610288f 100644 --- a/.sqlx/query-41bc2a929838b6becc9b3062d64d763abfc267a7804c3c1766f1ea498937dc4f.json +++ b/.sqlx/query-2164e9389cf2fe0a949600f45b9d82a33d58bf0c345ac61b65c1e278ad5307e6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT label AS \"label!:Label\"\n FROM messages\n WHERE organization_id = $1\n AND project_id = $2\n AND label IS NOT NULL\n ORDER BY label\n ", + "query": "\n SELECT DISTINCT label AS \"label!:Label\"\n FROM messages\n WHERE organization_id = $1 AND label IS NOT NULL\n ORDER BY label\n ", "describe": { "columns": [ { @@ -11,7 +11,6 @@ ], "parameters": { "Left": [ - "Uuid", "Uuid" ] }, @@ -19,5 +18,5 @@ true ] }, - "hash": "41bc2a929838b6becc9b3062d64d763abfc267a7804c3c1766f1ea498937dc4f" + "hash": "2164e9389cf2fe0a949600f45b9d82a33d58bf0c345ac61b65c1e278ad5307e6" } diff --git a/.sqlx/query-627c5920d7e5416c50f4ec71a8ae6ec034612dca650e047d359d4ec7d1a9246c.json b/.sqlx/query-2daf4bc3a73109dfbd4602d8a1b5b98a3afeaa8a360cc6b975d5169c5624be53.json similarity index 88% rename from .sqlx/query-627c5920d7e5416c50f4ec71a8ae6ec034612dca650e047d359d4ec7d1a9246c.json rename to .sqlx/query-2daf4bc3a73109dfbd4602d8a1b5b98a3afeaa8a360cc6b975d5169c5624be53.json index 0a803bca..5c2432c8 100644 --- a/.sqlx/query-627c5920d7e5416c50f4ec71a8ae6ec034612dca650e047d359d4ec7d1a9246c.json +++ b/.sqlx/query-2daf4bc3a73109dfbd4602d8a1b5b98a3afeaa8a360cc6b975d5169c5624be53.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n id,\n organization_id,\n project_id,\n smtp_credential_id,\n api_key_id,\n status AS \"status: _\",\n reason,\n delivery_details,\n from_email,\n recipients,\n ''::bytea AS \"raw_data!\",\n NULL::jsonb AS \"message_data\",\n octet_length(raw_data) AS \"raw_size!\",\n message_id_header,\n created_at,\n updated_at,\n retry_after,\n attempts,\n max_attempts,\n label AS \"label:Label\"\n FROM messages m\n WHERE organization_id = $1\n AND project_id = $2\n AND ($4::message_status[] IS NULL OR status = ANY($4))\n AND ($5::timestamptz IS NULL OR created_at <= $5)\n AND ($6::text[] IS NULL OR label = ANY($6))\n AND octet_length(raw_data) > 0 -- don't show deleted messages\n ORDER BY created_at DESC\n LIMIT $3\n ", + "query": "\n SELECT\n id,\n organization_id,\n project_id,\n smtp_credential_id,\n api_key_id,\n status AS \"status: _\",\n reason,\n delivery_details,\n from_email,\n recipients,\n ''::bytea AS \"raw_data!\",\n NULL::jsonb AS \"message_data\",\n octet_length(raw_data) AS \"raw_size!\",\n message_id_header,\n created_at,\n updated_at,\n retry_after,\n attempts,\n max_attempts,\n label AS \"label:Label\"\n FROM messages m\n WHERE organization_id = $1\n AND ($2::uuid IS NULL OR project_id = $2)\n AND ($3::message_status[] IS NULL OR status = ANY($3))\n AND ($4::timestamptz IS NULL OR created_at <= $4)\n AND ($5::text[] IS NULL OR label = ANY($5))\n AND octet_length(raw_data) > 0 -- don't show deleted messages\n ORDER BY created_at DESC\n LIMIT $6\n ", "describe": { "columns": [ { @@ -123,7 +123,6 @@ "Left": [ "Uuid", "Uuid", - "Int8", { "Custom": { "name": "message_status[]", @@ -148,7 +147,8 @@ } }, "Timestamptz", - "TextArray" + "TextArray", + "Int8" ] }, "nullable": [ @@ -174,5 +174,5 @@ true ] }, - "hash": "627c5920d7e5416c50f4ec71a8ae6ec034612dca650e047d359d4ec7d1a9246c" + "hash": "2daf4bc3a73109dfbd4602d8a1b5b98a3afeaa8a360cc6b975d5169c5624be53" } diff --git a/.sqlx/query-83ff89816add414b2b8cd70ddd5ecc4a0f531ada0c6b222912ab4baab9f1d813.json b/.sqlx/query-61f1a536b2f384a880a1a80183eb495fd812a2d61331310c8a7f12fc7e1674d3.json similarity index 92% rename from .sqlx/query-83ff89816add414b2b8cd70ddd5ecc4a0f531ada0c6b222912ab4baab9f1d813.json rename to .sqlx/query-61f1a536b2f384a880a1a80183eb495fd812a2d61331310c8a7f12fc7e1674d3.json index 94dc4a23..5233dbb0 100644 --- a/.sqlx/query-83ff89816add414b2b8cd70ddd5ecc4a0f531ada0c6b222912ab4baab9f1d813.json +++ b/.sqlx/query-61f1a536b2f384a880a1a80183eb495fd812a2d61331310c8a7f12fc7e1674d3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n m.id,\n m.organization_id,\n m.project_id,\n m.smtp_credential_id,\n m.api_key_id,\n m.status as \"status: _\",\n m.reason,\n m.delivery_details,\n m.from_email,\n m.recipients,\n -- Only return the first API_RAW_TRUNCATE_LENGTH bytes/ASCII-characters of the raw data.\n substring(m.raw_data FOR $4) as \"raw_data!\",\n octet_length(m.raw_data) as \"raw_size!\",\n m.message_data,\n m.message_id_header,\n m.created_at,\n m.updated_at,\n m.retry_after,\n m.attempts,\n m.max_attempts,\n m.label AS \"label:Label\"\n FROM messages m\n WHERE m.id = $1\n AND m.organization_id = $2\n AND m.project_id = $3\n AND octet_length(m.raw_data) > 0 -- don't show deleted messages\n ", + "query": "\n SELECT\n m.id,\n m.organization_id,\n m.project_id,\n m.smtp_credential_id,\n m.api_key_id,\n m.status as \"status: _\",\n m.reason,\n m.delivery_details,\n m.from_email,\n m.recipients,\n -- Only return the first API_RAW_TRUNCATE_LENGTH bytes/ASCII-characters of the raw data.\n substring(m.raw_data FOR $3) as \"raw_data!\",\n octet_length(m.raw_data) as \"raw_size!\",\n m.message_data,\n m.message_id_header,\n m.created_at,\n m.updated_at,\n m.retry_after,\n m.attempts,\n m.max_attempts,\n m.label AS \"label:Label\"\n FROM messages m\n WHERE m.id = $1\n AND m.organization_id = $2\n AND octet_length(m.raw_data) > 0 -- don't show deleted messages\n ", "describe": { "columns": [ { @@ -121,7 +121,6 @@ ], "parameters": { "Left": [ - "Uuid", "Uuid", "Uuid", "Int4" @@ -150,5 +149,5 @@ true ] }, - "hash": "83ff89816add414b2b8cd70ddd5ecc4a0f531ada0c6b222912ab4baab9f1d813" + "hash": "61f1a536b2f384a880a1a80183eb495fd812a2d61331310c8a7f12fc7e1674d3" } diff --git a/.sqlx/query-14a62a3a727307e357841676756ecff016d7f9f1958a20d0f35edac7043e271d.json b/.sqlx/query-72d09375994accc07bf4d65dc2c6dd4ae73180f2c3c6646298f66a82d398dad0.json similarity index 66% rename from .sqlx/query-14a62a3a727307e357841676756ecff016d7f9f1958a20d0f35edac7043e271d.json rename to .sqlx/query-72d09375994accc07bf4d65dc2c6dd4ae73180f2c3c6646298f66a82d398dad0.json index b29cc671..00119096 100644 --- a/.sqlx/query-14a62a3a727307e357841676756ecff016d7f9f1958a20d0f35edac7043e271d.json +++ b/.sqlx/query-72d09375994accc07bf4d65dc2c6dd4ae73180f2c3c6646298f66a82d398dad0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE messages\n SET raw_data = '',\n message_data = NULL,\n recipients = '{}',\n delivery_details = '{}'\n WHERE id = $1\n AND organization_id = $2\n AND project_id = $3\n RETURNING id\n ", + "query": "\n UPDATE messages\n SET raw_data = '',\n message_data = NULL,\n recipients = '{}',\n delivery_details = '{}'\n WHERE id = $1 AND organization_id = $2\n RETURNING id\n ", "describe": { "columns": [ { @@ -11,7 +11,6 @@ ], "parameters": { "Left": [ - "Uuid", "Uuid", "Uuid" ] @@ -20,5 +19,5 @@ false ] }, - "hash": "14a62a3a727307e357841676756ecff016d7f9f1958a20d0f35edac7043e271d" + "hash": "72d09375994accc07bf4d65dc2c6dd4ae73180f2c3c6646298f66a82d398dad0" } diff --git a/.sqlx/query-2b8067e776808e7cda898bfbd9b9e6b2c023ad2ae85ddc84cac5eca371309d57.json b/.sqlx/query-99f9ced07e12f98012dfb179ce8c240b888faa743875fd376362566efe9a5c58.json similarity index 80% rename from .sqlx/query-2b8067e776808e7cda898bfbd9b9e6b2c023ad2ae85ddc84cac5eca371309d57.json rename to .sqlx/query-99f9ced07e12f98012dfb179ce8c240b888faa743875fd376362566efe9a5c58.json index e92e1394..9859dd88 100644 --- a/.sqlx/query-2b8067e776808e7cda898bfbd9b9e6b2c023ad2ae85ddc84cac5eca371309d57.json +++ b/.sqlx/query-99f9ced07e12f98012dfb179ce8c240b888faa743875fd376362566efe9a5c58.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.status AS \"status:MessageStatus\"\n FROM messages m\n WHERE m.organization_id = $1\n AND m.project_id = $2\n AND m.id = $3\n ", + "query": "\n SELECT m.status AS \"status:MessageStatus\"\n FROM messages m\n WHERE m.organization_id = $1 AND m.id = $2\n ", "describe": { "columns": [ { @@ -26,7 +26,6 @@ ], "parameters": { "Left": [ - "Uuid", "Uuid", "Uuid" ] @@ -35,5 +34,5 @@ false ] }, - "hash": "2b8067e776808e7cda898bfbd9b9e6b2c023ad2ae85ddc84cac5eca371309d57" + "hash": "99f9ced07e12f98012dfb179ce8c240b888faa743875fd376362566efe9a5c58" } diff --git a/frontend/src/apiMiddleware.ts b/frontend/src/apiMiddleware.ts index 2bd6e4da..038ce04a 100644 --- a/frontend/src/apiMiddleware.ts +++ b/frontend/src/apiMiddleware.ts @@ -104,6 +104,10 @@ export default async function apiMiddleware( type: "set_domains", domains: await get(`/api/organizations/${newOrgId}/domains`), }); + dispatch({ + type: "set_labels", + labels: await get(`/api/organizations/${newOrgId}/emails/labels`), + }); } if (navState.to.name == "statistics") { @@ -115,23 +119,23 @@ export default async function apiMiddleware( type: "set_credentials", credentials: await get(`/api/organizations/${newOrgId}/projects/${newProjId}/smtp_credentials`), }); - dispatch({ - type: "set_labels", - labels: await get(`/api/organizations/${newOrgId}/projects/${newProjId}/labels`), - }); } let emailFilterChanged = false; const emailFilter = new URLSearchParams(); - for (const param of ["limit", "status", "before", "labels"]) { + for (const param of ["limit", "status", "before", "labels", "project"]) { const value = navState.to.params[param]; if (value != navState.from.params[param]) emailFilterChanged = true; if (value) emailFilter.append(param, value); } + if (navState.to.params.proj_id) { + // force project filter when path contains a project id (for viewing emails within a project) + emailFilter.set("project", navState.to.params.proj_id); + } if ((projChanged || emailFilterChanged || navState.to.params.force == "reload") && newProjId) { dispatch({ type: "set_emails", - emailMetadata: await get(`/api/organizations/${newOrgId}/projects/${newProjId}/emails?${emailFilter.toString()}`), + emailMetadata: await get(`/api/organizations/${newOrgId}/emails?${emailFilter.toString()}`), }); } diff --git a/frontend/src/components/emails/EmailDeleteButton.tsx b/frontend/src/components/emails/EmailDeleteButton.tsx index b017e9dd..292d87b1 100644 --- a/frontend/src/components/emails/EmailDeleteButton.tsx +++ b/frontend/src/components/emails/EmailDeleteButton.tsx @@ -3,7 +3,6 @@ import { IconTrash } from "@tabler/icons-react"; import { EmailMetadata } from "../../types.ts"; import { modals } from "@mantine/modals"; import { useOrganizations } from "../../hooks/useOrganizations.ts"; -import { useProjects } from "../../hooks/useProjects.ts"; import { notifications } from "@mantine/notifications"; import { useRemails } from "../../hooks/useRemails.ts"; import { errorNotification } from "../../notify.tsx"; @@ -11,20 +10,16 @@ import { MaintainerActionIcon, MaintainerButton } from "../RoleButtons.tsx"; export default function EmailDeleteButton({ email, small }: { email: EmailMetadata; small?: boolean }) { const { currentOrganization } = useOrganizations(); - const { currentProject } = useProjects(); const { navigate, dispatch } = useRemails(); - if (!currentOrganization || !currentProject) { + if (!currentOrganization) { return null; } const deleteEmail = async () => { - const res = await fetch( - `/api/organizations/${currentOrganization.id}/projects/${currentProject.id}/emails/${email.id}`, - { - method: "DELETE", - } - ); + const res = await fetch(`/api/organizations/${currentOrganization.id}/emails/${email.id}`, { + method: "DELETE", + }); if (res.status === 200) { notifications.show({ title: "Email deleted", diff --git a/frontend/src/components/emails/EmailRetryButton.tsx b/frontend/src/components/emails/EmailRetryButton.tsx index c12dfca6..0d650dc9 100644 --- a/frontend/src/components/emails/EmailRetryButton.tsx +++ b/frontend/src/components/emails/EmailRetryButton.tsx @@ -2,7 +2,6 @@ import { notifications } from "@mantine/notifications"; import { Email, EmailMetadata } from "../../types.ts"; import { IconReload } from "@tabler/icons-react"; import { useOrganizations } from "../../hooks/useOrganizations.ts"; -import { useProjects } from "../../hooks/useProjects.ts"; import { is_in_the_future } from "../../util.ts"; import { errorNotification } from "../../notify.tsx"; import { MaintainerActionIcon, MaintainerButton } from "../RoleButtons.tsx"; @@ -18,14 +17,13 @@ export default function EmailRetryButton({ small?: boolean; }) { const { currentOrganization } = useOrganizations(); - const { currentProject } = useProjects(); const [loading, setLoading] = useState(false); - if (!currentOrganization || !currentProject) { + if (!currentOrganization) { return null; } - const email_endpoint = `/api/organizations/${currentOrganization.id}/projects/${currentProject.id}/emails/${email.id}`; + const email_endpoint = `/api/organizations/${currentOrganization.id}/emails/${email.id}`; async function retry() { const res = await fetch(`${email_endpoint}/retry`, { diff --git a/frontend/src/hooks/useEmails.ts b/frontend/src/hooks/useEmails.ts index 2b119015..786bcf41 100644 --- a/frontend/src/hooks/useEmails.ts +++ b/frontend/src/hooks/useEmails.ts @@ -1,14 +1,12 @@ import { useRemails } from "./useRemails.ts"; import { useEffect, useState } from "react"; import { useOrganizations } from "./useOrganizations.ts"; -import { useProjects } from "./useProjects.ts"; import { Email, EmailMetadata } from "../types.ts"; import { RemailsError } from "../error/error.ts"; import { useSelector } from "./useSelector.ts"; export function useEmails() { const { currentOrganization } = useOrganizations(); - const { currentProject } = useProjects(); const labels = useSelector((s) => s.labels || []); const [currentEmail, setCurrentEmail] = useState(null); const { @@ -20,10 +18,8 @@ export function useEmails() { if (routerState.params.email_id) { const partialEmail = emails?.find((m) => m.id === routerState.params.email_id) || null; setCurrentEmail(partialEmail); - if (currentOrganization && currentProject) { - fetch( - `/api/organizations/${currentOrganization.id}/projects/${currentProject.id}/emails/${routerState.params.email_id}` - ) + if (currentOrganization) { + fetch(`/api/organizations/${currentOrganization.id}/emails/${routerState.params.email_id}`) .then((res) => { if (res.ok) { return res.json(); @@ -45,7 +41,7 @@ export function useEmails() { setCurrentEmail(null); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentOrganization, currentProject, routerState.params.email_id]); // don't update on `emails` + }, [currentOrganization, routerState.params.email_id]); // don't update on `emails` function updateEmail(email_id: string, update: Partial) { if (currentEmail?.id == email_id) { diff --git a/src/api/api_keys.rs b/src/api/api_keys.rs index c1c74faf..1c4cc51e 100644 --- a/src/api/api_keys.rs +++ b/src/api/api_keys.rs @@ -418,21 +418,17 @@ mod tests { // list messages let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails" - )) + .get(format!("/api/organizations/{org_1}/emails")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec = deserialize_body(response.into_body()).await; - assert_eq!(messages.len(), 5); + assert_eq!(messages.len(), 8); // get a specific message let message_1 = "e165562a-fb6d-423b-b318-fd26f4610634".parse().unwrap(); let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails/{message_1}" - )) + .get(format!("/api/organizations/{org_1}/emails/{message_1}")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); @@ -441,18 +437,14 @@ mod tests { // delete a message let response = server - .delete(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails/{message_1}" - )) + .delete(format!("/api/organizations/{org_1}/emails/{message_1}")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // check that is was removed let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails/{message_1}" - )) + .get(format!("/api/organizations/{org_1}/emails/{message_1}")) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); @@ -611,9 +603,7 @@ mod tests { // Read-only API keys should not be able to delete messages let message_1 = "e165562a-fb6d-423b-b318-fd26f4610634"; let response = server - .delete(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails/{message_1}" - )) + .delete(format!("/api/organizations/{org_1}/emails/{message_1}")) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); @@ -627,18 +617,14 @@ mod tests { // Read-only API keys are able to list messages let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails" - )) + .get(format!("/api/organizations/{org_1}/emails")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Read-only API keys are able to view messages let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails/{message_1}" - )) + .get(format!("/api/organizations/{org_1}/emails/{message_1}")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); diff --git a/src/api/messages.rs b/src/api/messages.rs index aa5c021a..aac1f8c5 100644 --- a/src/api/messages.rs +++ b/src/api/messages.rs @@ -273,13 +273,13 @@ pub async fn create_message( /// List all email messages /// -/// By default, the 10 most recently created messages are returned. To retrieve more on a single request, please set +/// By default, the 10 most recently created messages are returned. To retrieve more on a single request, set /// the query parameter `limit` between 1 and 100. Pagination is achieved via the `before` query /// parameter, i.e., to get older messages, please set the `before` param to the oldest `created_at` /// of the previous request. #[utoipa::path( get, - path = "/organizations/{org_id}/projects/{project_id}/emails", + path = "/organizations/{org_id}/emails", params(MessageFilter), tags = ["Emails"], responses( @@ -289,20 +289,17 @@ pub async fn create_message( )] pub async fn list_messages( State(repo): State, - Path((org_id, project_id)): Path<(OrganizationId, ProjectId)>, + Path(org_id): Path, ValidatedQuery(filter): ValidatedQuery, user: Box, ) -> ApiResult> { user.has_org_read_access(&org_id)?; - let messages = repo - .list_message_metadata(org_id, project_id, filter) - .await?; + let messages = repo.list_message_metadata(org_id, filter).await?; debug!( user_id = user.log_id(), organization_id = org_id.to_string(), - project_id = project_id.to_string(), "listed {} messages", messages.len() ); @@ -317,7 +314,7 @@ pub async fn list_messages( /// was actually truncated or did fit into the 10,000-character limit. #[utoipa::path( get, - path = "/organizations/{org_id}/projects/{project_id}/emails/{message_id}", + path = "/organizations/{org_id}/emails/{message_id}", tags = ["Emails"], responses( (status = 200, description = "Successfully fetched message", body = ApiMessage), @@ -326,17 +323,16 @@ pub async fn list_messages( )] pub async fn get_message( State(repo): State, - Path((org_id, project_id, message_id)): Path<(OrganizationId, ProjectId, MessageId)>, + Path((org_id, message_id)): Path<(OrganizationId, MessageId)>, user: Box, ) -> ApiResult { user.has_org_read_access(&org_id)?; - let message = repo.find_by_id(org_id, project_id, message_id).await?; + let message = repo.find_by_id(org_id, message_id).await?; debug!( user_id = user.log_id(), organization_id = org_id.to_string(), - project_id = project_id.to_string(), message_id = message_id.to_string(), "retrieved message", ); @@ -347,7 +343,7 @@ pub async fn get_message( /// Delete email message #[utoipa::path( delete, - path = "/organizations/{org_id}/projects/{project_id}/emails/{message_id}", + path = "/organizations/{org_id}/emails/{message_id}", tags = ["Emails"], responses( (status = 200, description = "Successfully deleted message", body = MessageId), @@ -356,17 +352,16 @@ pub async fn get_message( )] pub async fn remove_message( State(repo): State, - Path((org_id, project_id, message_id)): Path<(OrganizationId, ProjectId, MessageId)>, + Path((org_id, message_id)): Path<(OrganizationId, MessageId)>, user: Box, ) -> ApiResult { user.has_org_write_access(&org_id)?; - let id = repo.remove(org_id, project_id, message_id).await?; + let id = repo.remove(org_id, message_id).await?; debug!( user_id = user.log_id(), organization_id = org_id.to_string(), - project_id = project_id.to_string(), message_id = message_id.to_string(), "removed message", ); @@ -381,7 +376,7 @@ pub async fn remove_message( /// who have not previously generated a permanent failure response. #[utoipa::path( put, - path = "/organizations/{org_id}/projects/{project_id}/emails/{message_id}/retry", + path = "/organizations/{org_id}/emails/{message_id}/retry", tags = ["Emails"], responses( (status = 200, description = "Successfully initiated retry"), @@ -391,12 +386,12 @@ pub async fn remove_message( pub async fn retry_now( State(repo): State, State(bus_client): State>, - Path((org_id, project_id, message_id)): Path<(OrganizationId, ProjectId, MessageId)>, + Path((org_id, message_id)): Path<(OrganizationId, MessageId)>, user: Box, ) -> Result<(), AppError> { user.has_org_write_access(&org_id)?; - let status = repo.message_status(org_id, project_id, message_id).await?; + let status = repo.message_status(org_id, message_id).await?; if status == MessageStatus::Delivered { warn!( @@ -421,7 +416,6 @@ pub async fn retry_now( debug!( user_id = user.log_id(), organization_id = org_id.to_string(), - project_id = project_id.to_string(), message_id = message_id.to_string(), "requested message retry", ); @@ -434,7 +428,7 @@ pub async fn retry_now( /// Lists all labels that exist on at least one email message within that project. #[utoipa::path( get, - path = "/organizations/{org_id}/projects/{project_id}/labels", + path = "/organizations/{org_id}/emails/labels", tags = ["Emails"], responses( (status = 200, description = "Successfully fetched labels", body = [Label]), @@ -443,11 +437,11 @@ pub async fn retry_now( )] pub async fn list_labels( State(repo): State, - Path((org_id, project_id)): Path<(OrganizationId, ProjectId)>, + Path(org_id): Path, user: Box, ) -> ApiResult> { user.has_org_read_access(&org_id)?; - let labels = repo.list_labels(org_id, project_id).await?; + let labels = repo.list_labels(org_id).await?; Ok(Json(labels)) } @@ -494,69 +488,67 @@ mod tests { // list messages let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails" - )) + .get(format!("/api/organizations/{org_1}/emails")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec = deserialize_body(response.into_body()).await; - let messages_in_fixture = 5; + let messages_in_fixture = 8; assert_eq!(messages.len(), messages_in_fixture); - // filter by single label + // filter by project let response = server .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails?labels=label-1" + "/api/organizations/{org_1}/emails?project={proj_1}" )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec = deserialize_body(response.into_body()).await; - assert_eq!(messages.len(), 3); + assert_eq!(messages.len(), 5); - // filter by multiple labels + // filter by single label let response = server .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails?labels=label-1,label-2" + "/api/organizations/{org_1}/emails?project={proj_1}&labels=label-1" )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec = deserialize_body(response.into_body()).await; - assert_eq!(messages.len(), 4); + assert_eq!(messages.len(), 3); - // filter without labels + // filter by multiple labels let response = server .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/emails" + "/api/organizations/{org_1}/emails?project={proj_1}&labels=label-1,label-2" )) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec = deserialize_body(response.into_body()).await; - assert_eq!(messages.len(), 5); + assert_eq!(messages.len(), 4); // list labels let response = server - .get(format!( - "/api/organizations/{org_1}/projects/{proj_1}/labels" - )) + .get(format!("/api/organizations/{org_1}/emails/labels")) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let messages: Vec