diff --git a/.github/workflows/labrinth-build.yml b/.github/workflows/labrinth-build.yml
index 5fdc1ce080..7dfcffce83 100644
--- a/.github/workflows/labrinth-build.yml
+++ b/.github/workflows/labrinth-build.yml
@@ -8,6 +8,9 @@ on:
paths:
- .github/workflows/labrinth-build.yml
- 'apps/labrinth/**'
+ - 'packages/**'
+ - '!packages/api-client/**'
+ - '!packages/app-lib/**'
- Cargo.toml
- Cargo.lock
pull_request:
@@ -15,6 +18,9 @@ on:
paths:
- .github/workflows/labrinth-build.yml
- 'apps/labrinth/**'
+ - 'packages/**'
+ - '!packages/api-client/**'
+ - '!packages/app-lib/**'
- Cargo.toml
- Cargo.lock
merge_group:
diff --git a/Cargo.lock b/Cargo.lock
index db6f7b0ae2..6a53ac4b9b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2331,15 +2331,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
-[[package]]
-name = "convert_case"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
-dependencies = [
- "unicode-segmentation",
-]
-
[[package]]
name = "convert_case"
version = "0.10.0"
@@ -5217,15 +5208,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
-[[package]]
-name = "iso8601"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46"
-dependencies = [
- "nom 8.0.0",
-]
-
[[package]]
name = "itertools"
version = "0.12.1"
@@ -5385,19 +5367,6 @@ dependencies = [
"serde_json",
]
-[[package]]
-name = "jsonwebtoken"
-version = "9.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
-dependencies = [
- "base64 0.22.1",
- "js-sys",
- "ring",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -5491,7 +5460,6 @@ dependencies = [
"json-patch 4.1.0",
"labrinth",
"lettre",
- "meilisearch-sdk",
"modrinth-content-management",
"modrinth-util",
"muralpay",
@@ -5955,49 +5923,6 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
-[[package]]
-name = "meilisearch-index-setting-macro"
-version = "0.30.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e36e5cad3754ed329feb201c24f26c0694442bef50a90dbcff0ba6d12b5ca133"
-dependencies = [
- "convert_case 0.8.0",
- "proc-macro2",
- "quote",
- "structmeta",
- "syn 2.0.106",
-]
-
-[[package]]
-name = "meilisearch-sdk"
-version = "0.30.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f620c9ed9b790cf292b66c8dab16f0e4aafb377583ec0e589da7c3ee81ff2038"
-dependencies = [
- "async-trait",
- "bytes",
- "either",
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-util",
- "iso8601",
- "jsonwebtoken",
- "log",
- "meilisearch-index-setting-macro",
- "pin-project-lite",
- "reqwest 0.12.24",
- "serde",
- "serde_json",
- "thiserror 2.0.17",
- "time",
- "tokio",
- "uuid 1.23.3",
- "wasm-bindgen-futures",
- "web-sys",
- "yaup",
-]
-
[[package]]
name = "memchr"
version = "2.7.6"
@@ -13499,17 +13424,6 @@ dependencies = [
"xml-rs",
]
-[[package]]
-name = "yaup"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0144f1a16a199846cb21024da74edd930b43443463292f536b7110b4855b5c6"
-dependencies = [
- "form_urlencoded",
- "serde",
- "thiserror 1.0.69",
-]
-
[[package]]
name = "yoke"
version = "0.8.0"
diff --git a/Cargo.toml b/Cargo.toml
index 01c9a1af38..6670a240af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -131,7 +131,6 @@ lz4_flex = { version = "0.11.5", default-features = false, features = [
"std",
] }
maxminddb = "0.26.0"
-meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-content-management = { path = "packages/modrinth-content-management" }
modrinth-log = { path = "packages/modrinth-log" }
modrinth-util = { path = "packages/modrinth-util" }
diff --git a/README.md b/README.md
index 7b4f90ef20..0def9bf509 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
## Modrinth Monorepo
-Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
+Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app).
diff --git a/apps/app-frontend/src/components/ui/modal/ModrinthAccountRequiredModal.vue b/apps/app-frontend/src/components/ui/modal/ModrinthAccountRequiredModal.vue
index 18b832278d..ad413f1ab8 100644
--- a/apps/app-frontend/src/components/ui/modal/ModrinthAccountRequiredModal.vue
+++ b/apps/app-frontend/src/components/ui/modal/ModrinthAccountRequiredModal.vue
@@ -73,7 +73,7 @@
@@ -2063,7 +2114,7 @@ function copyId() {
v-tooltip="`Copy code`"
type="quiet"
:label="`Copy code`"
- class="absolute right-2 top-2 border-[1px]"
+ class="!absolute right-2 top-2 border-[1px]"
@click="copyToClipboard(getClassDecompiledSource(classItem)!, classItem.key)"
>
diff --git a/apps/frontend/src/components/ui/moderation/ModpackScanModal.vue b/apps/frontend/src/components/ui/moderation/ModpackScanModal.vue
index 41c0888f81..c3057c955c 100644
--- a/apps/frontend/src/components/ui/moderation/ModpackScanModal.vue
+++ b/apps/frontend/src/components/ui/moderation/ModpackScanModal.vue
@@ -11,6 +11,7 @@ import { Button, IconButton } from '@modrinth/ui'
import {
Combobox,
type ComboboxOption,
+ commonMessages,
ConfirmModal,
defineMessages,
injectModrinthClient,
@@ -61,10 +62,6 @@ const messages = defineMessages({
id: 'modpack-scan-modal.scanning',
defaultMessage: 'Scanning...',
},
- success: {
- id: 'modpack-scan-modal.success',
- defaultMessage: 'Success',
- },
failed: {
id: 'modpack-scan-modal.failed',
defaultMessage: 'Failed',
@@ -102,10 +99,6 @@ const messages = defineMessages({
defaultMessage:
'This will permanently delete all attribution groups for this project and all files inside them. This action cannot be undone.',
},
- deleteAllGroupsConfirmationProceed: {
- id: 'modpack-scan-modal.delete-all-groups.proceed',
- defaultMessage: 'Clear',
- },
deleteAllGroupsSuccess: {
id: 'modpack-scan-modal.delete-all-groups.success',
defaultMessage: 'All groups cleared successfully.',
@@ -344,7 +337,7 @@ async function clearAllGroups() {
if (!failed) {
addNotification({
type: 'success',
- title: formatMessage(messages.success),
+ title: formatMessage(commonMessages.successLabel),
text: formatMessage(messages.deleteAllGroupsSuccess),
})
}
@@ -369,7 +362,7 @@ defineExpose({ show, hide })
ref="clearModalRef"
:title="formatMessage(messages.deleteAllGroupsConfirmationTitle)"
:description="formatMessage(messages.deleteAllGroupsConfirmationDescription)"
- :proceed-label="formatMessage(messages.deleteAllGroupsConfirmationProceed)"
+ :proceed-label="formatMessage(commonMessages.clearButton)"
@proceed="clearAllGroups"
/>
diff --git a/apps/frontend/src/components/ui/moderation/settings/ModerationKeybind.vue b/apps/frontend/src/components/ui/moderation/settings/ModerationKeybind.vue
index b9dd7a5320..377c658252 100644
--- a/apps/frontend/src/components/ui/moderation/settings/ModerationKeybind.vue
+++ b/apps/frontend/src/components/ui/moderation/settings/ModerationKeybind.vue
@@ -2,9 +2,13 @@
+
{{ props.title }}
+
+
+
+
+ {{ formatMessage(messages.title) }}
+
+
+ {{ formatMessage(messages.uploadVersionFirst) }}
+
+
+
+
+
-
+
+
+
+ {{ formatMessage(messages.featuredTags) }}
+
+
+ {{ current.featuredTags.length }}/{{ MAX_FEATURED_TAGS }}
+
+
+
+ {{ formatMessage(messages.featuredTagsDescription) }}
+
+
+ {{ formatMessage(messages.selectAtLeastOneCategory) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/pages/[type]/[project]/version/[version].vue b/apps/frontend/src/pages/[type]/[project]/version/[version].vue
index 25fd9b2851..193aeb89f7 100644
--- a/apps/frontend/src/pages/[type]/[project]/version/[version].vue
+++ b/apps/frontend/src/pages/[type]/[project]/version/[version].vue
@@ -144,6 +144,22 @@
{{ formatMessage(commonMessages.downloadButton) }}
+
+
+ Open
+
+
+
+
{{ formatMessage(commonMessages.betaRelease) }}
diff --git a/apps/frontend/src/pages/collection/[collection].vue b/apps/frontend/src/pages/collection/[collection].vue
index 167ce44566..355c6abc24 100644
--- a/apps/frontend/src/pages/collection/[collection].vue
+++ b/apps/frontend/src/pages/collection/[collection].vue
@@ -425,6 +425,7 @@ import {
ProjectCardList,
RadioButtons,
SidebarCard,
+ sortProjectTypes,
StyledInput,
TeleportOverflowMenu,
useCompactNumber,
@@ -769,7 +770,7 @@ const projectTypes = computed(() => {
projects.value?.map((project) => project?.project_type).filter((x) => x !== undefined) || [],
)
projectSet.delete('project')
- return Array.from(projectSet)
+ return sortProjectTypes(projectSet)
})
function getProjectTypeSentenceMessage(type) {
diff --git a/apps/frontend/src/pages/dashboard/revenue/index.vue b/apps/frontend/src/pages/dashboard/revenue/index.vue
index 60b503a931..82f4c48756 100644
--- a/apps/frontend/src/pages/dashboard/revenue/index.vue
+++ b/apps/frontend/src/pages/dashboard/revenue/index.vue
@@ -159,22 +159,22 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M319.052 54.2233C319.058 37.6952 315.689 21.3441 309.156 6.19864C302.624 -8.94682 293.07 -22.559 281.094 -33.7816C269.119 -45.0042 254.982 -53.5944 239.573 -59.012C224.164 -64.4295 207.815 -66.5571 191.556 -65.2609C175.297 -63.9648 159.479 -59.2729 145.097 -51.4805C130.715 -43.688 118.08 -32.9636 107.987 -19.9818C97.8942 -6.99995 90.5617 7.95837 86.4509 23.9523C82.3401 39.9462 81.5398 56.6297 84.1004 72.9533L103.415 67.7101C100.452 45.7823 104.805 23.4811 115.783 4.35031C126.761 -14.7805 143.734 -29.6435 164.005 -37.8768C184.275 -46.1102 206.681 -47.2415 227.661 -41.0911C248.641 -34.9407 266.991 -21.8613 279.797 -3.93146L262.376 6.25239C255.476 -2.83248 246.698 -10.2779 236.659 -15.5617C226.619 -20.8455 215.561 -23.8398 204.26 -24.3345L206.032 -3.60929C217.266 -2.58081 227.949 1.79213 236.737 8.95915C245.524 16.1262 252.024 25.767 255.418 36.6684C258.812 47.5697 258.949 59.2444 255.81 70.223C252.672 81.2017 246.398 90.9937 237.78 98.3668L248.048 116.384C261.575 105.867 271.303 91.124 275.725 74.437C280.146 57.7501 279.016 40.0505 272.507 24.079L289.873 13.9453C295.192 26.0533 298.028 39.1299 298.209 52.3816L319.052 54.2233Z"
- fill="#1BD96A"
+ fill="var(--color-brand)"
fill-opacity="0.16"
/>
diff --git a/apps/frontend/src/pages/discover.vue b/apps/frontend/src/pages/discover.vue
index 7b0f537acb..74f3699db8 100644
--- a/apps/frontend/src/pages/discover.vue
+++ b/apps/frontend/src/pages/discover.vue
@@ -1,12 +1,15 @@
-
+
{{ formatMessage(commonMessages.betaRelease) }}
@@ -110,7 +110,7 @@
{{ formatMessage(messages.whyModrinthHosting) }}
@@ -154,7 +154,7 @@
-
+
{{ formatMessage(messages.yourFavoriteMods) }}
{{ formatMessage(messages.yourFavoriteModsDescription) }}
@@ -238,7 +238,7 @@
{{ formatMessage(messages.includedWithYourServer) }}
@@ -641,9 +641,9 @@ import {
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
- LoaderIcon,
ModrinthServersPurchaseModal,
OptionGroup,
+ TagIcon,
useFormatPrice,
useVIntl,
} from '@modrinth/ui'
diff --git a/apps/frontend/src/pages/moderation.vue b/apps/frontend/src/pages/moderation.vue
index 9352acabba..4334b52079 100644
--- a/apps/frontend/src/pages/moderation.vue
+++ b/apps/frontend/src/pages/moderation.vue
@@ -1,5 +1,8 @@
-
+
Moderation
@@ -16,6 +19,7 @@
diff --git a/apps/frontend/src/pages/moderation/technical-review/index.vue b/apps/frontend/src/pages/moderation/technical-review/index.vue
index 23877ace6b..b46ed59776 100644
--- a/apps/frontend/src/pages/moderation/technical-review/index.vue
+++ b/apps/frontend/src/pages/moderation/technical-review/index.vue
@@ -32,6 +32,7 @@ useHead({ title: 'Tech review queue - Modrinth' })
const client = injectModrinthClient()
const queryClient = useQueryClient()
+const keybinds = useModerationKeybinds()
const currentPage = ref(1)
const API_PAGE_SIZE = 50
@@ -571,6 +572,50 @@ watch(totalPages, (pages) => {
// complete: 20,
// }
// })
+
+const CARD_BOTTOM_OFFSET = 210
+
+function handleKeybinds(event: KeyboardEvent) {
+ keybinds.value.handle(event, {
+ scope: 'tech-review',
+ actions: {
+ goToTop: () => {
+ Array.from(cardRefs.values())
+ .filter((card) => card.getBoundingClientRect().top <= 0)
+ .reduce((prev, curr) =>
+ curr.getBoundingClientRect().top > prev.getBoundingClientRect().top ? curr : prev,
+ )
+ ?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ },
+ goToBottom: () => {
+ const nearestCard = Array.from(cardRefs.values())
+ .filter((card) => card.getBoundingClientRect().bottom >= window.innerHeight)
+ .reduce((prev, curr) =>
+ curr.getBoundingClientRect().top < prev.getBoundingClientRect().top ? curr : prev,
+ )
+
+ if (nearestCard) {
+ window.scrollTo({
+ behavior: 'smooth',
+ top:
+ nearestCard.getBoundingClientRect().bottom +
+ window.scrollY -
+ window.innerHeight +
+ CARD_BOTTOM_OFFSET,
+ })
+ }
+ },
+ },
+ })
+}
+
+onMounted(() => {
+ window.addEventListener('keydown', handleKeybinds)
+})
+
+onUnmounted(() => {
+ window.removeEventListener('keydown', handleKeybinds)
+})
diff --git a/apps/frontend/src/pages/organization/[organization].vue b/apps/frontend/src/pages/organization/[organization].vue
index 15a805eeff..609d3c8e4c 100644
--- a/apps/frontend/src/pages/organization/[organization].vue
+++ b/apps/frontend/src/pages/organization/[organization].vue
@@ -236,6 +236,7 @@ import {
ProjectCard,
ProjectCardList,
SidebarCard,
+ sortProjectTypes,
useCompactNumber,
useVIntl,
} from '@modrinth/ui'
@@ -386,15 +387,14 @@ const isInvited = computed(() => {
})
const projectTypes = computed(() => {
- const obj: Record = {}
+ const types = new Set()
for (const project of projects.value ?? []) {
- obj[project.project_types[0] ?? 'project'] = true
+ const type = project.project_types[0] ?? 'project'
+ if (type !== 'project') types.add(type)
}
- delete obj.project
-
- return Object.keys(obj)
+ return sortProjectTypes(types)
})
function isProjectServer(project: ProjectV3): boolean {
return project.minecraft_server != null
@@ -504,15 +504,12 @@ const navLinks = computed(() => [
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.value?.slug}`,
},
- ...projectTypes.value
- .map((x) => {
- return {
- label: formatMessage(getProjectTypeMessage(x as ProjectType, true)),
- href: `/organization/${organization.value?.slug}/${x}s`,
- }
- })
- .slice()
- .sort((a, b) => a.label.localeCompare(b.label)),
+ ...projectTypes.value.map((x) => {
+ return {
+ label: formatMessage(getProjectTypeMessage(x as ProjectType, true)),
+ href: `/organization/${organization.value?.slug}/${x}s`,
+ }
+ }),
])
async function copyId() {
diff --git a/apps/frontend/src/pages/user/[user].vue b/apps/frontend/src/pages/user/[user].vue
index 7f3921f4fe..c7f3ec27ed 100644
--- a/apps/frontend/src/pages/user/[user].vue
+++ b/apps/frontend/src/pages/user/[user].vue
@@ -35,6 +35,9 @@ const userProfile = provideUserProfile({
getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId),
getCollections: (userId) => client.labrinth.users_v2.getCollections(userId),
patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch),
+ changeAvatar: (userId, file, extension) =>
+ client.labrinth.users_v2.changeIcon(userId, file, extension),
+ deleteAvatar: (userId) => client.labrinth.users_v2.deleteIcon(userId),
getBlockedUsers: () => client.labrinth.blocked_users_v3.list(),
blockUser: (userId) => client.labrinth.blocked_users_v3.block(userId),
unblockUser: (userId) => client.labrinth.blocked_users_v3.unblock(userId),
diff --git a/apps/labrinth/.sqlx/query-6ebb0cffc875f50ebe95b91e4735f1c520155d65a5c51021224ab4461c688db8.json b/apps/labrinth/.sqlx/query-6ebb0cffc875f50ebe95b91e4735f1c520155d65a5c51021224ab4461c688db8.json
deleted file mode 100644
index 6d00c56a9f..0000000000
--- a/apps/labrinth/.sqlx/query-6ebb0cffc875f50ebe95b91e4735f1c520155d65a5c51021224ab4461c688db8.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT id FROM mods\n WHERE id = ANY($1)\n AND components ? 'minecraft_server'\n ",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8Array"
- ]
- },
- "nullable": [
- false
- ]
- },
- "hash": "6ebb0cffc875f50ebe95b91e4735f1c520155d65a5c51021224ab4461c688db8"
-}
diff --git a/apps/labrinth/.sqlx/query-93b19a49ae305ab760792d22c5b955a75323663880b47aa9ed42c9f33cd65f16.json b/apps/labrinth/.sqlx/query-93b19a49ae305ab760792d22c5b955a75323663880b47aa9ed42c9f33cd65f16.json
new file mode 100644
index 0000000000..3e0b89ec2e
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-93b19a49ae305ab760792d22c5b955a75323663880b47aa9ed42c9f33cd65f16.json
@@ -0,0 +1,28 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT v.id, v.mod_id\n FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n WHERE\n v.id = ANY($1)\n AND jsonb_typeof(m.components -> 'minecraft_server') IS DISTINCT FROM 'object'\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "mod_id",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8Array"
+ ]
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "93b19a49ae305ab760792d22c5b955a75323663880b47aa9ed42c9f33cd65f16"
+}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index fccb039a12..fbfe31b0bf 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -73,7 +73,6 @@ image = { workspace = true, features = [
itertools = { workspace = true }
json-patch = { workspace = true }
lettre = { workspace = true }
-meilisearch-sdk = { workspace = true, features = ["reqwest"] }
modrinth-content-management = { workspace = true }
modrinth-util = { workspace = true, features = ["decimal", "sentry", "utoipa"] }
muralpay = { workspace = true, features = ["client", "mock", "utoipa"] }
diff --git a/apps/labrinth/src/auth/checks.rs b/apps/labrinth/src/auth/checks.rs
index ea3d1e259b..daf9d35d00 100644
--- a/apps/labrinth/src/auth/checks.rs
+++ b/apps/labrinth/src/auth/checks.rs
@@ -11,6 +11,8 @@ use crate::models::projects::{
use crate::models::users::User;
use crate::queue::file_scan::get_files_missing_attribution;
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use futures::TryStreamExt;
use itertools::Itertools;
use xredis::RedisPool;
@@ -91,7 +93,8 @@ pub async fn filter_visible_projects(
pool,
hide_unlisted,
)
- .await?;
+ .await
+ .wrap_api_err("filtering visible project ids")?;
projects.retain(|x| filtered_project_ids.contains(&x.inner.id));
Ok(projects.into_iter().map(|x| x.into()).collect())
}
@@ -128,7 +131,8 @@ pub async fn filter_visible_project_ids(
if !check_projects.is_empty() {
return_projects.extend(
filter_enlisted_projects_ids(check_projects, user_option, pool)
- .await?,
+ .await
+ .wrap_api_err("filtering enlisted projects ids")?,
);
}
@@ -178,7 +182,8 @@ pub async fn filter_enlisted_projects_ids(
}
})
.try_collect::>()
- .await?;
+ .await
+ .wrap_internal_err("fetching query results from database")?;
}
Ok(return_projects)
}
@@ -218,7 +223,8 @@ pub async fn filter_visible_versions(
pool,
redis,
)
- .await?;
+ .await
+ .wrap_api_err("filtering visible version ids")?;
versions.retain(|x| filtered_version_ids.contains(&x.inner.id));
let version_ids: Vec<_> = versions.iter().map(|v| v.inner.id).collect();
@@ -265,10 +271,9 @@ impl ValidateAuthorized for models::DBOAuthClient {
return if user.role.is_mod() || user.id == self.created_by.into() {
Ok(())
} else {
- Err(ApiError::CustomAuthentication(
- "You don't have sufficient permissions to interact with this OAuth application"
- .to_string(),
- ))
+ Err(ApiError::Auth(eyre::eyre!(
+ "You don't have sufficient permissions to interact with this OAuth application",
+ )))
};
}
@@ -292,7 +297,8 @@ pub async fn filter_visible_version_ids(
// Get visible projects- ones we are allowed to see public versions for.
let visible_project_ids = filter_visible_project_ids(
DBProject::get_many_ids(&project_ids, pool, redis)
- .await?
+ .await
+ .wrap_api_err("fetching projects for visibility filtering")?
.iter()
.map(|x| &x.inner)
.collect(),
@@ -300,12 +306,14 @@ pub async fn filter_visible_version_ids(
pool,
false,
)
- .await?;
+ .await
+ .wrap_api_err("filtering visible project IDs")?;
// Then, get enlisted versions (Versions that are a part of a project we are a member of)
let enlisted_version_ids =
filter_enlisted_version_ids(versions.clone(), user_option, pool, redis)
- .await?;
+ .await
+ .wrap_api_err("filtering enlisted version ids")?;
let version_ids: Vec<_> = versions.iter().map(|v| v.id).collect();
let withheld_versions = get_files_missing_attribution(pool, &version_ids)
@@ -346,14 +354,16 @@ pub async fn filter_enlisted_version_ids(
// Get enlisted projects- ones we are allowed to see hidden versions for.
let authorized_project_ids = filter_enlisted_projects_ids(
DBProject::get_many_ids(&project_ids, pool, redis)
- .await?
+ .await
+ .wrap_api_err("fetching projects for membership filtering")?
.iter()
.map(|x| &x.inner)
.collect(),
user_option,
pool,
)
- .await?;
+ .await
+ .wrap_api_err("filtering projects by membership")?;
for version in versions {
if user_option.as_ref().is_some_and(|x| x.role.is_mod())
@@ -428,7 +438,8 @@ pub async fn is_visible_organization(
) -> Result {
let members =
DBTeamMember::get_from_team_full(organization.team_id, pool, redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching team members from database")?;
// This is meant to match the same projects as the `Project::is_searchable` method, but we're not using
// it here because that'd entail pulling in all projects for the organization
@@ -437,7 +448,7 @@ pub async fn is_visible_organization(
organization.id as database::models::ids::DBOrganizationId
)
.fetch_optional(pool)
- .await?
+ .await.wrap_internal_err("checking organization for searchable projects")?
.flatten()
.unwrap_or(false);
diff --git a/apps/labrinth/src/clickhouse/fetch.rs b/apps/labrinth/src/clickhouse/fetch.rs
index b0245075b8..73d9be0e52 100644
--- a/apps/labrinth/src/clickhouse/fetch.rs
+++ b/apps/labrinth/src/clickhouse/fetch.rs
@@ -1,3 +1,4 @@
+use crate::util::error::Context as _;
use std::sync::Arc;
use crate::{models::ids::ProjectId, routes::ApiError};
@@ -47,7 +48,10 @@ pub async fn fetch_playtimes(
.bind(end_date.timestamp())
.bind(projects.iter().map(|x| x.0).collect::>());
- Ok(query.fetch_all().await?)
+ query
+ .fetch_all()
+ .await
+ .wrap_internal_err("querying database for `fetch_playtimes`")
}
// Fetches views as a Vec of ReturnViews
@@ -77,7 +81,10 @@ pub async fn fetch_views(
.bind(end_date.timestamp())
.bind(projects.iter().map(|x| x.0).collect::>());
- Ok(query.fetch_all().await?)
+ query
+ .fetch_all()
+ .await
+ .wrap_internal_err("querying database for `fetch_views`")
}
// Fetches downloads as a Vec of ReturnDownloads
@@ -106,7 +113,10 @@ pub async fn fetch_downloads(
.bind(end_date.timestamp())
.bind(projects.iter().map(|x| x.0).collect::>());
- Ok(query.fetch_all().await?)
+ query
+ .fetch_all()
+ .await
+ .wrap_internal_err("querying database for `fetch_downloads`")
}
pub async fn fetch_countries_downloads(
@@ -133,7 +143,10 @@ pub async fn fetch_countries_downloads(
.bind(end_date.timestamp())
.bind(projects.iter().map(|x| x.0).collect::>());
- Ok(query.fetch_all().await?)
+ query
+ .fetch_all()
+ .await
+ .wrap_internal_err("querying database for `fetch_countries_downloads`")
}
pub async fn fetch_countries_views(
@@ -160,5 +173,8 @@ pub async fn fetch_countries_views(
.bind(end_date.timestamp())
.bind(projects.iter().map(|x| x.0).collect::>());
- Ok(query.fetch_all().await?)
+ query
+ .fetch_all()
+ .await
+ .wrap_internal_err("querying database for `fetch_countries_views`")
}
diff --git a/apps/labrinth/src/database/models/notifications_template_item.rs b/apps/labrinth/src/database/models/notifications_template_item.rs
index dbe908e39e..3c9ef97f70 100644
--- a/apps/labrinth/src/database/models/notifications_template_item.rs
+++ b/apps/labrinth/src/database/models/notifications_template_item.rs
@@ -1,7 +1,8 @@
use crate::database::models::DatabaseError;
use crate::models::v3::notifications::{NotificationChannel, NotificationType};
use crate::routes::ApiError;
-use crate::util::error::Context;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use serde::{Deserialize, Serialize};
use xredis::RedisPool;
@@ -140,7 +141,11 @@ where
drop(redis_conn);
- let cached = HtmlBody { html: get().await? };
+ let cached = HtmlBody {
+ html: get()
+ .await
+ .wrap_api_err("generating notification template HTML")?,
+ };
let mut redis_conn = redis.connect().await.wrap_internal_err(
"connecting to redis for dynamic notification html",
)?;
diff --git a/apps/labrinth/src/database/models/products_tax_identifier_item.rs b/apps/labrinth/src/database/models/products_tax_identifier_item.rs
index 506a82ce2e..0a8a5a85e5 100644
--- a/apps/labrinth/src/database/models/products_tax_identifier_item.rs
+++ b/apps/labrinth/src/database/models/products_tax_identifier_item.rs
@@ -1,6 +1,7 @@
use crate::database::models::ids::{DBProductId, DBProductPriceId};
use crate::models::billing::ProductMetadata;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
pub struct DBProductsTaxIdentifier {
pub id: i32,
@@ -18,7 +19,8 @@ impl DBProductsTaxIdentifier {
product_id.0,
)
.fetch_optional(exec)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_product`")?;
Ok(maybe_row.map(|row| DBProductsTaxIdentifier {
id: row.id,
@@ -41,7 +43,8 @@ impl DBProductsTaxIdentifier {
price_id.0,
)
.fetch_optional(exec)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_price`")?;
Ok(maybe_row.map(|row| DBProductsTaxIdentifier {
id: row.id,
@@ -73,7 +76,7 @@ pub async fn product_info_by_product_price_id(
product_price_id.0 as i64,
)
.fetch_optional(exec)
- .await?;
+ .await.wrap_internal_err("querying database for `product_info_by_product_price_id`")?;
match maybe_row {
None => Ok(None),
@@ -83,7 +86,8 @@ pub async fn product_info_by_product_price_id(
tax_processor_id: row.tax_processor_id,
product_id: DBProductId(row.product_id),
},
- product_metadata: serde_json::from_value(row.product_metadata)?,
+ product_metadata: serde_json::from_value(row.product_metadata)
+ .wrap_request_err("deserializing JSON data")?,
})),
}
}
diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs
index e3ffe16b75..382dd9fc66 100644
--- a/apps/labrinth/src/lib.rs
+++ b/apps/labrinth/src/lib.rs
@@ -328,16 +328,16 @@ pub fn app_data_config(
labrinth_config: LabrinthConfig,
) {
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
- routes::ApiError::Validation(err.to_string()).into()
+ routes::ApiError::Request(eyre::eyre!("{err}")).into()
}))
.app_data(web::PathConfig::default().error_handler(|err, _req| {
- routes::ApiError::Validation(err.to_string()).into()
+ routes::ApiError::Request(eyre::eyre!("{err}")).into()
}))
.app_data(web::QueryConfig::default().error_handler(|err, _req| {
- routes::ApiError::Validation(err.to_string()).into()
+ routes::ApiError::Request(eyre::eyre!("{err}")).into()
}))
.app_data(web::JsonConfig::default().error_handler(|err, _req| {
- routes::ApiError::Validation(err.to_string()).into()
+ routes::ApiError::Request(eyre::eyre!("{err}")).into()
}))
.app_data(web::Data::new(labrinth_config.redis_pool.clone()))
.app_data(web::Data::new(labrinth_config.pool.clone()))
diff --git a/apps/labrinth/src/models/v3/moderation_notes.rs b/apps/labrinth/src/models/v3/moderation_notes.rs
index b6621dc8ea..5488ec0ddf 100644
--- a/apps/labrinth/src/models/v3/moderation_notes.rs
+++ b/apps/labrinth/src/models/v3/moderation_notes.rs
@@ -4,6 +4,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::routes::ApiError;
+use crate::util::error::Context as _;
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ModerationNote {
@@ -37,9 +38,9 @@ pub struct PatchModerationNote {
impl PatchModerationNote {
pub fn validate_not_empty(&self) -> Result<(), ApiError> {
if self.notes.is_none() && self.user_rating.is_none() {
- return Err(ApiError::InvalidInput(
- "must specify `notes` or `user_rating`".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "must specify `notes` or `user_rating`",
+ )));
}
Ok(())
@@ -53,16 +54,14 @@ pub fn parse_if_match_header(
return Ok(None);
};
- let value = value.to_str().map_err(|_| {
- ApiError::InvalidInput(
- "`if-match` header must be a valid integer".to_string(),
- )
- })?;
+ let value = value.to_str().wrap_request_err(
+ "`if-match` header must be a valid integer".to_string(),
+ )?;
Some(value.parse::().map_err(|_| {
- ApiError::InvalidInput(
- "`if-match` header must be a valid integer".to_string(),
- )
+ ApiError::Request(eyre::eyre!(
+ "`if-match` header must be a valid integer",
+ ))
}))
.transpose()
}
diff --git a/apps/labrinth/src/models/v3/notifications.rs b/apps/labrinth/src/models/v3/notifications.rs
index 12598e7fed..2b9cb26dee 100644
--- a/apps/labrinth/src/models/v3/notifications.rs
+++ b/apps/labrinth/src/models/v3/notifications.rs
@@ -767,8 +767,8 @@ impl NotificationDeliveryStatus {
NotificationDeliveryStatus::Delivered => Ok(()),
NotificationDeliveryStatus::SkippedPreferences |
NotificationDeliveryStatus::SkippedDefault |
- NotificationDeliveryStatus::Pending => Err(ApiError::InvalidInput("An error occurred while sending an email to your email address. Please try again later.".to_owned())),
- NotificationDeliveryStatus::PermanentlyFailed => Err(ApiError::InvalidInput("This email address doesn't exist! Please try another one.".to_owned())),
+ NotificationDeliveryStatus::Pending => Err(ApiError::Request(eyre::eyre!("An error occurred while sending an email to your email address. Please try again later.".to_owned()))),
+ NotificationDeliveryStatus::PermanentlyFailed => Err(ApiError::Request(eyre::eyre!("This email address doesn't exist! Please try another one.".to_owned()))),
}
}
diff --git a/apps/labrinth/src/queue/analytics/mod.rs b/apps/labrinth/src/queue/analytics/mod.rs
index 91c3f553d2..f1be710b4b 100644
--- a/apps/labrinth/src/queue/analytics/mod.rs
+++ b/apps/labrinth/src/queue/analytics/mod.rs
@@ -4,7 +4,7 @@ use crate::models::analytics::{
};
use crate::routes::ApiError;
use crate::routes::analytics::MINECRAFT_SERVER_PLAYS;
-use crate::util::error::Context;
+use crate::util::error::Context as _;
use dashmap::{DashMap, DashSet};
use std::collections::HashMap;
use tracing::trace;
@@ -100,25 +100,39 @@ impl AnalyticsQueue {
if !affiliate_code_clicks_queue.is_empty() {
let mut insert_clicks = client
.insert::("affiliate_code_clicks")
- .await?;
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
for (_, click_vec) in affiliate_code_clicks_queue {
for click in click_vec {
- insert_clicks.write(&click).await?;
+ insert_clicks.write(&click).await.wrap_internal_err(
+ "writing analytics data to ClickHouse",
+ )?;
}
}
- insert_clicks.end().await?;
+ insert_clicks
+ .end()
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
}
if !playtime_queue.is_empty() {
- let mut playtimes = client.insert::("playtime").await?;
+ let mut playtimes = client
+ .insert::("playtime")
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
for playtime in playtime_queue {
- playtimes.write(&playtime).await?;
+ playtimes.write(&playtime).await.wrap_internal_err(
+ "writing analytics data to ClickHouse",
+ )?;
}
- playtimes.end().await?;
+ playtimes
+ .end()
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
}
if !minecraft_server_plays_queue.is_empty() {
@@ -176,13 +190,19 @@ impl AnalyticsQueue {
let mut plays = client
.insert::(MINECRAFT_SERVER_PLAYS)
- .await?;
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
for (_, play) in raw_plays {
- plays.write(&play).await?;
+ plays.write(&play).await.wrap_internal_err(
+ "writing analytics data to ClickHouse",
+ )?;
}
- plays.end().await?;
+ plays
+ .end()
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
}
if !views_queue.is_empty() {
@@ -242,7 +262,10 @@ impl AnalyticsQueue {
.wrap_internal_err("writing view count to redis")?;
}
- let mut views = client.insert::("views").await?;
+ let mut views = client
+ .insert::("views")
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
for (all_views, monetized) in raw_views {
for (idx, mut view) in all_views.into_iter().enumerate() {
@@ -250,11 +273,16 @@ impl AnalyticsQueue {
view.monetized = false;
}
- views.write(&view).await?;
+ views.write(&view).await.wrap_internal_err(
+ "writing analytics data to ClickHouse",
+ )?;
}
}
- views.end().await?;
+ views
+ .end()
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
}
if !downloads_queue.is_empty() {
@@ -308,8 +336,14 @@ impl AnalyticsQueue {
.wrap_internal_err("writing download count to redis")?;
}
- let mut transaction = pool.begin().await?;
- let mut downloads = client.insert::("downloads").await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ let mut downloads = client
+ .insert::("downloads")
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
let mut version_downloads: HashMap = HashMap::new();
let mut project_downloads: HashMap = HashMap::new();
@@ -329,7 +363,9 @@ impl AnalyticsQueue {
trace!("writing download {download:?}");
- downloads.write(&download).await?;
+ downloads.write(&download).await.wrap_internal_err(
+ "writing analytics data to ClickHouse",
+ )?;
}
sqlx::query!(
@@ -343,7 +379,8 @@ impl AnalyticsQueue {
&version_downloads.values().copied().collect::>(),
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("incrementing version download counts")?;
sqlx::query!(
"
@@ -356,10 +393,17 @@ impl AnalyticsQueue {
&project_downloads.values().copied().collect::>(),
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("incrementing project download counts")?;
- transaction.commit().await?;
- downloads.end().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
+ downloads
+ .end()
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
}
Ok(())
diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs
index f982ce48f8..504720adb4 100644
--- a/apps/labrinth/src/queue/billing.rs
+++ b/apps/labrinth/src/queue/billing.rs
@@ -24,6 +24,7 @@ use crate::routes::internal::billing::payments::*;
use crate::util::anrok;
use crate::util::archon::ArchonClient;
use crate::util::archon::{CreateServerRequest, Specs};
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use ariadne::ids::base62_impl::to_base62;
use chrono::Utc;
@@ -48,9 +49,14 @@ async fn update_tax_amounts(
let mut processed_charges = 0;
loop {
- let mut txn = pg.begin().await?;
+ let mut txn = pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- let charges = DBCharge::get_updateable_lock(&mut txn, 5).await?;
+ let charges = DBCharge::get_updateable_lock(&mut txn, 5)
+ .await
+ .wrap_internal_err("fetching charges from database")?;
if charges.is_empty() {
info!("No more charges to process");
@@ -82,67 +88,70 @@ async fn update_tax_amounts(
charge.price_id,
&pg,
)
- .await?
+ .await
+ .wrap_api_err("fetching price")?
.ok_or_else(|| {
DatabaseError::Database(sqlx::Error::RowNotFound)
- })?;
+ })
+ .wrap_internal_err("querying database for `update_tax_amounts`")?;
- let product =
- DBProduct::get_price(charge.price_id, &pg)
- .await?
- .ok_or_else(|| {
- DatabaseError::Database(
- sqlx::Error::RowNotFound,
- )
- })?;
+ let product = DBProduct::get_price(charge.price_id, &pg)
+ .await
+ .wrap_internal_err(
+ "fetching product price from database",
+ )?
+ .ok_or_else(|| {
+ DatabaseError::Database(sqlx::Error::RowNotFound)
+ })
+ .wrap_internal_err(
+ "finding product price in database",
+ )?;
let stripe_address = 'a: {
- let stripe_customer_id =
+ let stripe_customer =
DBUser::get_id(charge.user_id, &pg, &redis)
- .await?
- .ok_or_else(|| {
- ApiError::from(DatabaseError::Database(
- sqlx::Error::RowNotFound,
- ))
- })
- .and_then(|user| {
- user.stripe_customer_id.ok_or_else(
- || {
- ApiError::InvalidInput(
- "User has no Stripe customer ID"
- .to_owned(),
- )
- },
- )
- })?
- .parse()
- .map_err(|_| {
- ApiError::InvalidInput(
- "User Stripe customer ID was invalid".to_owned(),
- )
- })?;
+ .await
+ .wrap_internal_err(
+ "fetching Stripe customer from database",
+ )?
+ .wrap_internal_err(
+ "finding Stripe customer in database",
+ )?;
+ let stripe_customer_id = stripe_customer
+ .stripe_customer_id
+ .wrap_request_err(
+ "finding Stripe customer ID on user",
+ )?
+ .parse()
+ .wrap_request_err(
+ "parsing user Stripe customer ID",
+ )?;
let customer = stripe::Customer::retrieve(
&stripe_client,
&stripe_customer_id,
&["invoice_settings.default_payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
// A customer should have a default payment method if they have an active subscription.
let payment_method = customer
.invoice_settings
.and_then(|x| {
- x.default_payment_method.and_then(|x| x.into_object())
+ x.default_payment_method
+ .and_then(|x| x.into_object())
})
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Customer has no default payment method!".to_string(),
- )
+ .wrap_request_err_with(|| {
+ "customer has no default payment method!"
+ .to_string()
})?;
- let stripe_address = payment_method.billing_details.address;
+ let stripe_address =
+ payment_method.billing_details.address;
// Attempt the default payment method's address first, then the customer's address.
match stripe_address {
@@ -152,18 +161,14 @@ async fn update_tax_amounts(
}
};
- customer.address.ok_or_else(|| {
- ApiError::InvalidInput(
- "Couldn't get an address for the Stripe customer"
- .to_owned(),
- )
+ customer.address.wrap_request_err_with(|| {
+ "couldn't get an address for the Stripe customer"
+ .to_owned()
})?
};
let customer_address =
- anrok::Address::from_stripe_address(
- &stripe_address,
- );
+ anrok::Address::from_stripe_address(&stripe_address);
let tax_amount = anrok_client
.create_ephemeral_txn(&anrok::TransactionFields {
@@ -179,17 +184,16 @@ async fn update_tax_amounts(
customer_id: None,
customer_name: None,
})
- .await?
+ .await
+ .wrap_internal_err("inserting database records for `update_tax_amounts`")?
.tax_amount_to_collect;
- Result::::Ok(
- ProcessedCharge {
- new_tax_amount: tax_amount,
- product_name: product
- .name
- .unwrap_or_else(|| "Modrinth".to_owned()),
- },
- )
+ Result::::Ok(ProcessedCharge {
+ new_tax_amount: tax_amount,
+ product_name: product
+ .name
+ .unwrap_or_else(|| "Modrinth".to_owned()),
+ })
};
op_fut.then(move |res| async move { (charge_clone, res) })
@@ -212,11 +216,9 @@ async fn update_tax_amounts(
// for this.
let subscription_id =
- charge.subscription_id.ok_or_else(|| {
- ApiError::InvalidInput(
- "Charge has no subscription ID".to_owned(),
- )
- })?;
+ charge.subscription_id.wrap_request_err_with(
+ || "charge has no subscription ID".to_owned(),
+ )?;
NotificationBuilder {
body: NotificationBody::TaxNotification {
@@ -234,7 +236,8 @@ async fn update_tax_amounts(
},
}
.insert(charge.user_id, &mut txn, redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `update_tax_amounts`")?;
charge.tax_amount = new_tax_amount;
}
@@ -248,10 +251,15 @@ async fn update_tax_amounts(
};
charge.tax_last_updated = Some(Utc::now());
- charge.upsert(&mut txn).await?;
+ charge
+ .upsert(&mut txn)
+ .await
+ .wrap_internal_err("updating subscription id in database")?;
}
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
if processed_charges >= limit {
break Ok(());
@@ -287,11 +295,9 @@ async fn update_anrok_transactions(
.payment_platform_id
.as_ref()
.and_then(|x| x.parse().ok())
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Refund charge has no or an invalid refund ID"
- .to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "refund charge has no or an invalid refund ID"
+ .to_owned()
})?;
let refund = stripe::Refund::retrieve(
@@ -299,15 +305,16 @@ async fn update_anrok_transactions(
&refund_id,
&["payment_intent.payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let pi = refund
.payment_intent
.and_then(|x| x.into_object())
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Refund charge has no payment intent".to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "refund charge has no payment intent".to_owned()
})?;
(pi, anrok::transaction_id_stripe_pyr(&refund_id))
@@ -316,10 +323,8 @@ async fn update_anrok_transactions(
.payment_platform_id
.as_ref()
.and_then(|x| x.parse().ok())
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Charge has no payment platform ID".to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "charge has no payment platform ID".to_owned()
})?;
// Attempt retrieving the address via the payment intent's payment method
@@ -329,7 +334,10 @@ async fn update_anrok_transactions(
&stripe_id,
&["payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let anrok_id = anrok::transaction_id_stripe_pi(&stripe_id);
@@ -341,27 +349,17 @@ async fn update_anrok_transactions(
.and_then(|x| x.into_object())
.and_then(|x| x.billing_details.address);
- let stripe_customer_id =
- DBUser::get_id(c.user_id, &mut *txn, redis)
- .await?
- .ok_or_else(|| {
- ApiError::from(DatabaseError::Database(
- sqlx::Error::RowNotFound,
- ))
- })
- .and_then(|user| {
- user.stripe_customer_id.ok_or_else(|| {
- ApiError::InvalidInput(
- "User has no Stripe customer ID".to_owned(),
- )
- })
- })?;
+ let stripe_customer = DBUser::get_id(c.user_id, &mut *txn, redis)
+ .await
+ .wrap_internal_err("fetching Stripe customer from database")?
+ .wrap_internal_err("finding Stripe customer in database")?;
+ let stripe_customer_id = stripe_customer
+ .stripe_customer_id
+ .wrap_request_err("finding Stripe customer ID on user")?;
- let customer_id = stripe_customer_id.parse().map_err(|e| {
- ApiError::InvalidInput(format!(
- "Charge's Stripe customer ID was invalid ({e})"
- ))
- })?;
+ let customer_id = stripe_customer_id
+ .parse()
+ .wrap_request_err("parsing request value")?;
match pi_stripe_address {
Some(address) => {
@@ -377,7 +375,10 @@ async fn update_anrok_transactions(
let customer =
stripe::Customer::retrieve(stripe_client, &customer_id, &[])
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let Some(address) = customer.address else {
// We won't really be able to do anything about this.
@@ -388,7 +389,9 @@ async fn update_anrok_transactions(
);
c.tax_platform_id = Some("unresolved".to_owned());
- c.upsert(txn).await?;
+ c.upsert(txn)
+ .await
+ .wrap_internal_err("updating customer in database")?;
return Ok(());
};
@@ -397,8 +400,12 @@ async fn update_anrok_transactions(
};
let tax_id = DBProductsTaxIdentifier::get_price(c.price_id, &mut *txn)
- .await?
- .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?;
+ .await
+ .wrap_api_err("fetching price")?
+ .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err(
+ "fetching products tax identifier from database",
+ )?;
// Note: if the tax amount that was charged to the customer is *different* than
// what it *should* be NOW, we will take on a loss here.
@@ -427,18 +434,18 @@ async fn update_anrok_transactions(
match result {
Ok(response) => {
- let version = response.version.ok_or_else(|| {
- ApiError::InvalidInput(
- "Anrok response is missing tax transaction version"
- .to_owned(),
- )
+ let version = response.version.wrap_request_err_with(|| {
+ "anrok response is missing tax transaction version"
+ .to_owned()
})?;
c.tax_drift_loss = Some(response.tax_amount_to_collect);
c.tax_platform_id = Some(tax_platform_id);
c.tax_transaction_version = Some(version);
c.tax_platform_accounting_time = Some(c.due);
- c.upsert(txn).await?;
+ c.upsert(txn)
+ .await
+ .wrap_internal_err("updating version in database")?;
Ok(())
}
@@ -449,11 +456,15 @@ async fn update_anrok_transactions(
.is_conflict_and(|x| x == "customerAddressCouldNotResolve")
{
c.tax_platform_id = Some("unresolved".to_owned());
- c.upsert(txn).await?;
+ c.upsert(txn)
+ .await
+ .wrap_internal_err("updating version in database")?;
Ok(())
} else {
- Err(error.into())
+ Err(ApiError::Internal(eyre::eyre!(
+ "calculating tax with Anrok: {error}"
+ )))
}
}
}
@@ -464,11 +475,15 @@ async fn update_anrok_transactions(
let mut offset = 0;
loop {
- let mut txn = pg.begin().await?;
+ let mut txn = pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut charges =
DBCharge::get_missing_tax_identifier_lock(&mut txn, offset, 1)
- .await?;
+ .await
+ .wrap_internal_err("fetching charges from database")?;
let Some(c) = charges.pop() else {
info!("No more charges to process");
@@ -492,7 +507,9 @@ async fn update_anrok_transactions(
offset += 1;
}
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
if processed_charges >= limit {
break Ok(());
@@ -512,7 +529,10 @@ pub async fn try_process_user_redeemal(
user_redeemal.last_attempt = Some(Utc::now());
user_redeemal.n_attempts += 1;
user_redeemal.status = users_redeemals::Status::Processing;
- let updated = user_redeemal.update_status_if_pending(pool).await?;
+ let updated = user_redeemal
+ .update_status_if_pending(pool)
+ .await
+ .wrap_internal_err("updating updated in database")?;
if !updated {
return Ok(());
@@ -526,7 +546,8 @@ pub async fn try_process_user_redeemal(
product_item::QueryProductWithPrices::list_by_product_type(
pool, "medal",
)
- .await?;
+ .await
+ .wrap_internal_err("fetching query product with prices from Redis")?;
let Some(product_item::QueryProductWithPrices {
id: _product_id,
@@ -536,9 +557,9 @@ pub async fn try_process_user_redeemal(
name: _,
}) = medal_products.pop()
else {
- return Err(ApiError::Conflict(
+ return Err(ApiError::Conflict(eyre::eyre!(
"Missing Medal subscription product".to_owned(),
- ));
+ )));
};
let ProductMetadata::Medal {
@@ -549,31 +570,31 @@ pub async fn try_process_user_redeemal(
region,
} = metadata
else {
- return Err(ApiError::Conflict(
+ return Err(ApiError::Conflict(eyre::eyre!(
"Missing or incorrect metadata for Medal subscription".to_owned(),
- ));
+ )));
};
let Some(medal_price) = prices.pop() else {
- return Err(ApiError::Conflict(
+ return Err(ApiError::Conflict(eyre::eyre!(
"Missing price for Medal subscription".to_owned(),
- ));
+ )));
};
let (price_duration, price_amount) = match medal_price.prices {
Price::OneTime { price: _ } => {
- return Err(ApiError::Conflict(
+ return Err(ApiError::Conflict(eyre::eyre!(
"Unexpected metadata for Medal subscription price".to_owned(),
- ));
+ )));
}
Price::Recurring { intervals } => {
let Some((price_duration, price_amount)) =
intervals.into_iter().next()
else {
- return Err(ApiError::Conflict(
+ return Err(ApiError::Conflict(eyre::eyre!(
"Missing price interval for Medal subscription".to_owned(),
- ));
+ )));
};
(price_duration, price_amount)
@@ -585,13 +606,15 @@ pub async fn try_process_user_redeemal(
// Get the user's username
let user = DBUser::get_id(user_id, pool, redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .wrap_not_found_err("resource not found")?;
// Send the provision request to Archon. On failure, the redeemal will be "stuck" processing,
// and moved back to pending by `index_subscriptions`.
- let archon_client = ArchonClient::from_env()?;
+ let archon_client = ArchonClient::from_env()
+ .wrap_api_err("executing `ArchonClient::from_env`")?;
let server_id = archon_client
.create_server(&CreateServerRequest {
user_id: to_base62(user_id.0 as u64),
@@ -606,13 +629,21 @@ pub async fn try_process_user_redeemal(
region,
tags: vec!["medal".to_owned()],
})
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `try_process_user_redeemal`",
+ )?;
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
// Build a subscription using this price ID.
let subscription = DBUserSubscription {
- id: generate_user_subscription_id(&mut txn).await?,
+ id: generate_user_subscription_id(&mut txn)
+ .await
+ .wrap_internal_err("generating user subscription ID")?,
user_id,
price_id,
interval: PriceDuration::FiveDays,
@@ -623,12 +654,17 @@ pub async fn try_process_user_redeemal(
}),
};
- subscription.upsert(&mut txn).await?;
+ subscription
+ .upsert(&mut txn)
+ .await
+ .wrap_internal_err("generating user subscription ID")?;
// Insert an expiring charge, `index_subscriptions` will unprovision the
// subscription when expired.
DBCharge {
- id: generate_charge_id(&mut txn).await?,
+ id: generate_charge_id(&mut txn)
+ .await
+ .wrap_internal_err("generating redeemal charge ID")?,
user_id,
price_id,
amount: price_amount.into(),
@@ -651,26 +687,42 @@ pub async fn try_process_user_redeemal(
tax_platform_accounting_time: None,
}
.upsert(&mut txn)
- .await?;
+ .await
+ .wrap_internal_err("upserting redeemal charge")?;
// Update `users_redeemal`, mark subscription as redeemed.
user_redeemal.status = users_redeemals::Status::Processed;
- user_redeemal.update(&mut txn).await?;
+ user_redeemal.update(&mut txn).await.wrap_internal_err(
+ "updating database records for `try_process_user_redeemal`",
+ )?;
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
pub async fn cancel_failing_charges(pool: &PgPool) -> Result<(), ApiError> {
- let charges_to_cancel = DBCharge::get_cancellable(pool).await?;
+ let charges_to_cancel = DBCharge::get_cancellable(pool)
+ .await
+ .wrap_internal_err("fetching charge from database")?;
for mut charge in charges_to_cancel {
charge.status = ChargeStatus::Cancelled;
- let mut transaction = pool.begin().await?;
- charge.upsert(&mut transaction).await?;
- transaction.commit().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ charge
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("updating transaction in database")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
}
Ok(())
@@ -682,7 +734,9 @@ pub async fn process_chargeable_charges(
stripe_client: &stripe::Client,
anrok_client: &anrok::Client,
) -> Result<(), ApiError> {
- let charges_to_do = DBCharge::get_chargeable(pool).await?;
+ let charges_to_do = DBCharge::get_chargeable(pool)
+ .await
+ .wrap_internal_err("fetching charge from database")?;
let prices = product_item::DBProductPrice::get_many(
&charges_to_do
@@ -693,7 +747,8 @@ pub async fn process_chargeable_charges(
.collect::>(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching product prices from database")?;
let users = crate::database::models::DBUser::get_many_ids(
&charges_to_do
@@ -705,7 +760,8 @@ pub async fn process_chargeable_charges(
pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching users from database")?;
for mut charge in charges_to_do {
let Some(product_price) =
@@ -784,9 +840,18 @@ pub async fn process_chargeable_charges(
charge.status = ChargeStatus::Failed;
}
- let mut transaction = pool.begin().await?;
- charge.upsert(&mut transaction).await?;
- transaction.commit().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ charge
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("updating transaction in database")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
}
Ok(())
@@ -798,7 +863,10 @@ async fn unprovision_subscriptions(
) -> Result<(), ApiError> {
info!("Gathering charges to unprovision");
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut clear_cache_users = Vec::new();
// If an active subscription has:
@@ -806,7 +874,9 @@ async fn unprovision_subscriptions(
// - An expiring charge due now
// - A failed charge more than two days ago
// It should be unprovisioned
- let all_charges = DBCharge::get_unprovision(pool).await?;
+ let all_charges = DBCharge::get_unprovision(pool)
+ .await
+ .wrap_internal_err("fetching charges from database")?;
let mut all_subscriptions =
user_subscription_item::DBUserSubscription::get_many(
@@ -818,7 +888,8 @@ async fn unprovision_subscriptions(
.collect::>(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
let subscription_prices = product_item::DBProductPrice::get_many(
&all_subscriptions
.iter()
@@ -828,7 +899,8 @@ async fn unprovision_subscriptions(
.collect::>(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching product prices from database")?;
let subscription_products = product_item::DBProduct::get_many(
&subscription_prices
.iter()
@@ -838,7 +910,8 @@ async fn unprovision_subscriptions(
.collect::>(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching products from database")?;
let users = DBUser::get_many_ids(
&all_subscriptions
.iter()
@@ -849,7 +922,8 @@ async fn unprovision_subscriptions(
pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching users from database")?;
for charge in all_charges {
debug!("Unprovisioning charge '{}'", to_base62(charge.id.0 as u64));
@@ -898,7 +972,10 @@ async fn unprovision_subscriptions(
user.id as DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `unprovision_subscriptions`",
+ )?;
true
}
@@ -939,7 +1016,10 @@ async fn unprovision_subscriptions(
if unprovisioned {
subscription.status = SubscriptionStatus::Unprovisioned;
- subscription.upsert(&mut transaction).await?;
+ subscription
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("updating err in database")?;
DBUsersSubscriptionsAffiliations::deactivate(
subscription.id,
@@ -961,8 +1041,12 @@ async fn unprovision_subscriptions(
.collect::>(),
redis,
)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
@@ -972,11 +1056,17 @@ async fn process_redeemals(
redis: &RedisPool,
) -> Result<(), ApiError> {
// If an offer redeemal has been processing for over 5 minutes, it should be set pending.
- UserRedeemal::update_stuck_5_minutes(pool).await?;
+ UserRedeemal::update_stuck_5_minutes(pool)
+ .await
+ .wrap_internal_err(
+ "updating database records for `process_redeemals`",
+ )?;
// If an offer redeemal is pending, try processing it.
// Try processing it.
- let pending_redeemals = UserRedeemal::get_pending(pool, 100).await?;
+ let pending_redeemals = UserRedeemal::get_pending(pool, 100)
+ .await
+ .wrap_internal_err("fetching user redeemal from Redis")?;
for redeemal in pending_redeemals {
if let Err(error) =
try_process_user_redeemal(pool, redis, redeemal).await
diff --git a/apps/labrinth/src/queue/email.rs b/apps/labrinth/src/queue/email.rs
index ff1b9cfb7b..5ee5374b17 100644
--- a/apps/labrinth/src/queue/email.rs
+++ b/apps/labrinth/src/queue/email.rs
@@ -10,6 +10,8 @@ use crate::models::v3::notifications::{
NotificationChannel, NotificationDeliveryStatus,
};
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use chrono::Utc;
use futures::stream::{FuturesUnordered, StreamExt};
use lettre::message::Mailbox;
@@ -150,7 +152,13 @@ impl EmailQueue {
/// Returns `Ok(false)` if no emails were processed, `Ok(true)` if some were processed.
#[instrument(name = "EmailQueue::index", skip_all)]
pub async fn index(&self, limit: i64) -> Result {
- let transport = self.mailer.lock().await.to_transport().await?;
+ let transport = self
+ .mailer
+ .lock()
+ .await
+ .to_transport()
+ .await
+ .wrap_internal_err("creating email transport")?;
let begin = std::time::Instant::now();
@@ -159,7 +167,8 @@ impl EmailQueue {
limit,
&self.pg,
)
- .await?;
+ .await
+ .wrap_internal_err("creating email transport")?;
if deliveries.is_empty() {
return Ok(false);
@@ -171,7 +180,9 @@ impl EmailQueue {
// ballooning the error rate.
for d in deliveries.iter_mut().filter(|d| d.attempt_count >= 3) {
d.status = NotificationDeliveryStatus::PermanentlyFailed;
- d.update(&self.pg).await?;
+ d.update(&self.pg).await.wrap_internal_err(
+ "marking exhausted email delivery as failed",
+ )?;
}
// We hold a FOR UPDATE lock on the rows here, so no other workers are accessing them
@@ -183,7 +194,9 @@ impl EmailQueue {
.map(|d| d.notification_id)
.collect::>();
let notifications =
- DBNotification::get_many(¬ification_ids, &self.pg).await?;
+ DBNotification::get_many(¬ification_ids, &self.pg)
+ .await
+ .wrap_internal_err("fetching notifications from database")?;
// For all notifications we collected, fill out the template
// and send it via SMTP in parallel.
@@ -201,11 +214,16 @@ impl EmailQueue {
let seq = Arc::clone(&sequential_processing);
futures.push(async move {
- let mut txn = this.pg.begin().await?;
+ let mut txn = this
+ .pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let maybe_user =
DBUser::get_id(notification.user_id, &mut txn, &this.redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching user from database")?;
let Some(mailbox) = maybe_user
.and_then(|user| user.email)
@@ -268,7 +286,9 @@ impl EmailQueue {
};
delivery.attempt_count += 1;
- delivery.update(&self.pg).await?;
+ delivery.update(&self.pg).await.wrap_internal_err(
+ "updating processed email delivery",
+ )?;
}
}
@@ -283,7 +303,10 @@ impl EmailQueue {
delivery.next_attempt = Utc::now()
+ chrono::Duration::seconds(EMAIL_RETRY_DELAY_SECONDS);
- delivery.update(&self.pg).await?;
+ delivery
+ .update(&self.pg)
+ .await
+ .wrap_internal_err("scheduling email delivery retry")?;
}
info!(
@@ -302,7 +325,13 @@ impl EmailQueue {
user_id: DBUserId,
address: Mailbox,
) -> Result {
- let transport = self.mailer.lock().await.to_transport().await?;
+ let transport = self
+ .mailer
+ .lock()
+ .await
+ .to_transport()
+ .await
+ .wrap_internal_err("creating email transport")?;
self.send_one_with_transport(
txn,
transport,
@@ -329,7 +358,8 @@ impl EmailQueue {
&mut *txn,
&self.redis,
)
- .await?
+ .await
+ .wrap_internal_err("creating email transport")?
.into_iter()
.find(|t| t.notification_type == notification.notification_type()) else {
return Ok(NotificationDeliveryStatus::SkippedDefault);
@@ -345,7 +375,8 @@ impl EmailQueue {
self.identity.clone(),
address,
)
- .await?;
+ .await
+ .wrap_api_err("executing `templates::build_email`")?;
let send_result = transport.send(message).await;
diff --git a/apps/labrinth/src/queue/email/templates.rs b/apps/labrinth/src/queue/email/templates.rs
index 1ef37e77fe..1ba595cdae 100644
--- a/apps/labrinth/src/queue/email/templates.rs
+++ b/apps/labrinth/src/queue/email/templates.rs
@@ -11,6 +11,7 @@ use crate::database::models::{
use crate::env::ENV;
use crate::models::v3::notifications::NotificationBody;
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::http::HTTP_CLIENT;
use ariadne::ids::base62_impl::to_base62;
@@ -135,7 +136,11 @@ pub async fn build_email(
) -> Result {
let get_html_body = async {
let result: Result, ApiError> =
- match template.get_cached_html_data(redis).await? {
+ match template
+ .get_cached_html_data(redis)
+ .await
+ .wrap_internal_err("fetching email template HTML from Redis")?
+ {
Some(html_body) => Ok(Ok(html_body)),
None => {
let result = client
@@ -149,7 +154,10 @@ pub async fn build_email(
if let Ok(ref body) = result {
template
.set_cached_html_data(body.clone(), redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "updating database records for `build_email`",
+ )?;
}
Ok(result)
@@ -167,8 +175,10 @@ pub async fn build_email(
} = from;
let db_user = DBUser::get_id(user_id, &mut *exec, redis)
- .await?
- .ok_or(DatabaseError::Database(sqlx::Error::RowNotFound))?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or(DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err("fetching user from database")?;
let map = [
(USER_NAME, db_user.username),
@@ -180,17 +190,24 @@ pub async fn build_email(
let (html_body_result, either) = futures::try_join!(
get_html_body,
collect_template_variables(exec, redis, user_id, body, map)
- )?;
+ )
+ .wrap_api_err("executing `collect_template_variables`")?;
let mut message_builder = Message::builder().from(Mailbox::new(
Some(from_name),
- from_address.parse().map_err(MailError::from)?,
+ from_address
+ .parse()
+ .map_err(MailError::from)
+ .wrap_internal_err("reading HTTP response body")?,
));
if let Some((name, address)) = reply_name.zip(reply_address) {
message_builder = message_builder.reply_to(Mailbox::new(
Some(name),
- address.parse().map_err(MailError::from)?,
+ address
+ .parse()
+ .map_err(MailError::from)
+ .wrap_internal_err("reading HTTP response body")?,
));
}
@@ -244,21 +261,26 @@ pub async fn build_email(
html: Some(html),
} => message_builder
.multipart(MultiPart::alternative_plain_html(plaintext, html))
- .map_err(MailError::from)?,
+ .map_err(MailError::from)
+ .wrap_internal_err(
+ "executing `MultiPart::alternative_plain_html`",
+ )?,
Body {
plaintext: Some(plaintext),
html: None,
} => message_builder
.singlepart(SinglePart::plain(plaintext))
- .map_err(MailError::from)?,
+ .map_err(MailError::from)
+ .wrap_internal_err("executing `SinglePart::plain`")?,
Body {
plaintext: None,
html: Some(html),
} => message_builder
.singlepart(SinglePart::html(html))
- .map_err(MailError::from)?,
+ .map_err(MailError::from)
+ .wrap_internal_err("executing `SinglePart::html`")?,
Body {
plaintext: None,
@@ -329,7 +351,10 @@ async fn resolve_report_title(
return Ok(title);
}
- let Some(report) = DBReport::get(report_id, &mut *exec).await? else {
+ let Some(report) = DBReport::get(report_id, &mut *exec)
+ .await
+ .wrap_internal_err("fetching report from database")?
+ else {
return Ok(title);
};
let Some(shared_instance_id) = report.shared_instance_id else {
@@ -398,8 +423,10 @@ async fn collect_template_variables(
exec,
redis,
)
- .await?
- .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
+ .await
+ .wrap_api_err("fetching email project")?
+ .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err("fetching project from database")?
.inner;
map.insert(PROJECT_ID, to_base62(project_id.0));
@@ -423,7 +450,7 @@ async fn collect_template_variables(
report_id.0 as i64
)
.fetch_one(&mut *exec)
- .await?;
+ .await.wrap_internal_err("querying database for `collect_template_variables`")?;
map.insert(REPORT_ID, to_base62(report_id.0));
map.insert(
@@ -433,7 +460,8 @@ async fn collect_template_variables(
DBReportId(report_id.0 as i64),
result.title,
)
- .await?,
+ .await
+ .wrap_api_err("executing `resolve_report_title`")?,
);
map.insert(REPORT_DATE, date_human_readable(result.created));
Ok(EmailTemplate::Static(map))
@@ -453,7 +481,7 @@ async fn collect_template_variables(
report_id.0 as i64
)
.fetch_one(&mut *exec)
- .await?;
+ .await.wrap_internal_err("querying database for `collect_template_variables`")?;
map.insert(
REPORT_TITLE,
@@ -462,7 +490,8 @@ async fn collect_template_variables(
DBReportId(report_id.0 as i64),
result.title,
)
- .await?,
+ .await
+ .wrap_api_err("executing `resolve_report_title`")?,
);
map.insert(NEWREPORT_ID, to_base62(report_id.0));
Ok(EmailTemplate::Static(map))
@@ -476,7 +505,10 @@ async fn collect_template_variables(
project_id.0 as i64
)
.fetch_one(&mut *exec)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(PROJECT_ID, to_base62(project_id.0));
map.insert(PROJECT_NAME, result.name);
@@ -494,8 +526,10 @@ async fn collect_template_variables(
exec,
redis,
)
- .await?
- .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
+ .await
+ .wrap_api_err("fetching email project")?
+ .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err("fetching project from database")?
.inner;
map.insert(PROJECT_ID, to_base62(project_id.0));
@@ -516,8 +550,10 @@ async fn collect_template_variables(
&mut *exec,
redis,
)
- .await?
- .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
+ .await
+ .wrap_api_err("fetching email project")?
+ .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err("fetching project from database")?
.inner;
map.insert(PROJECT_ID, to_base62(project_id.0));
@@ -530,10 +566,14 @@ async fn collect_template_variables(
&mut *exec,
redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
.ok_or_else(|| {
DatabaseError::Database(sqlx::Error::RowNotFound)
- })?;
+ })
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(NEWOWNER_TYPE, "user".to_string());
map.insert(NEWOWNER_TYPE_CAPITALIZED, "User".to_string());
@@ -546,10 +586,14 @@ async fn collect_template_variables(
&mut *exec,
redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching organization from database")?
.ok_or_else(|| {
DatabaseError::Database(sqlx::Error::RowNotFound)
- })?;
+ })
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(NEWOWNER_TYPE, "organization".to_string());
map.insert(
@@ -584,7 +628,10 @@ async fn collect_template_variables(
user_id.0 as i64
)
.fetch_one(&mut *exec)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(TEAMINVITE_INVITER_NAME, result.inviter_name);
map.insert(TEAMINVITE_PROJECT_NAME, result.project_name);
@@ -616,7 +663,10 @@ async fn collect_template_variables(
user_id.0 as i64
)
.fetch_one(&mut *exec)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(ORGINVITE_INVITER_NAME, result.inviter_name);
map.insert(ORGINVITE_ORG_NAME, result.organization_name);
@@ -644,7 +694,10 @@ async fn collect_template_variables(
user_id.0 as i64,
)
.fetch_one(&mut *exec)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `collect_template_variables`",
+ )?;
map.insert(STATUSCHANGE_PROJECT_NAME, result.project_name);
map.insert(STATUSCHANGE_OLD_STATUS, old_status.as_str().to_owned());
@@ -829,7 +882,9 @@ async fn collect_template_variables(
key,
} => Ok(EmailTemplate::Dynamic {
variables: map,
- body: dynamic_email_body(redis, title, body_md, key).await?,
+ body: dynamic_email_body(redis, title, body_md, key)
+ .await
+ .wrap_api_err("executing `dynamic_email_body`")?,
title: title.to_string(),
}),
@@ -844,8 +899,10 @@ async fn collect_template_variables(
&mut *exec,
redis,
)
- .await?
- .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))
+ .wrap_internal_err("fetching user from database")?;
map.insert(SERVERINVITE_INVITER_NAME, inviter.username);
map.insert(SERVERINVITE_SERVER_NAME, server_name.clone());
@@ -883,9 +940,11 @@ async fn dynamic_email_body(
}))
.send()
.await
- .and_then(|res| res.error_for_status())?
+ .and_then(|res| res.error_for_status())
+ .wrap_internal_err("deserializing HTTP response")?
.bytes()
- .await?
+ .await
+ .wrap_internal_err("deserializing HTTP response")?
.as_ref(),
)
.wrap_internal_err("email body is not valid UTF-8")
diff --git a/apps/labrinth/src/queue/payouts/flow/mod.rs b/apps/labrinth/src/queue/payouts/flow/mod.rs
index 08c8d7776a..240b525acf 100644
--- a/apps/labrinth/src/queue/payouts/flow/mod.rs
+++ b/apps/labrinth/src/queue/payouts/flow/mod.rs
@@ -1,6 +1,7 @@
//! Centralized place where payout rails are defined - their fees, minimum and
//! maximum withdraw amounts, and execution logic.
+use crate::util::error::ApiContext as _;
use eyre::eyre;
use modrinth_util::decimal::Decimal2dp;
use rust_decimal::Decimal;
@@ -59,7 +60,9 @@ impl PayoutsQueue {
self,
withdrawal.amount,
method_details,
- &get_method.await?,
+ &get_method
+ .await
+ .wrap_api_err("executing `tremendous::create`")?,
)
.await
}
diff --git a/apps/labrinth/src/queue/payouts/flow/mural.rs b/apps/labrinth/src/queue/payouts/flow/mural.rs
index 15ff290e80..6cf2dad6f3 100644
--- a/apps/labrinth/src/queue/payouts/flow/mural.rs
+++ b/apps/labrinth/src/queue/payouts/flow/mural.rs
@@ -19,7 +19,7 @@ use crate::{
mural::MuralPayoutRequest,
},
routes::ApiError,
- util::error::Context,
+ util::error::{ApiContext as _, Context},
};
pub const PLATFORM_FEE: PayoutMethodFee = PayoutMethodFee {
@@ -67,7 +67,7 @@ pub(super) async fn create(
let mural = queue.muralpay.load();
let mural = mural
.as_ref()
- .wrap_internal_err("Mural client not available")?;
+ .wrap_internal_err("required Mural client is not available")?;
let method_fee_usd;
let forex_usd_to_currency;
@@ -158,14 +158,15 @@ pub(super) async fn execute(
recipient_info,
}: MuralFlow,
) -> Result<(), ApiError> {
- let user_email = get_verified_email(user)?;
+ let user_email = get_verified_email(user)
+ .wrap_api_err("fetching verified user email")?;
let sent_to_method_usd = net_usd + method_fee_usd;
let total_fee_usd = method_fee_usd + platform_fee_usd;
let mural = queue.muralpay.load();
let mural = mural
.as_ref()
- .wrap_internal_err("Mural client not available")?;
+ .wrap_internal_err("required Mural client is not available")?;
let payment_statement_doc = queue
.create_mural_payment_statement_doc(
@@ -175,7 +176,8 @@ pub(super) async fn execute(
&recipient_info,
gotenberg,
)
- .await?;
+ .await
+ .wrap_api_err("creating Mural payment statement document")?;
let user_id = UserId::from(user.id);
let method_id = match &payout_details {
@@ -235,13 +237,18 @@ pub(super) async fn execute(
Some(format!("User {user_id}")),
&[payout],
)
- .await
- .map_err(|err| match err {
- muralpay::MuralError::Api(err) => ApiError::Mural(Box::new(err)),
- err => ApiError::Internal(
+ .await;
+ let payout_request = match payout_request {
+ Ok(payout_request) => payout_request,
+ Err(muralpay::MuralError::Api(err)) => {
+ return Err(ApiError::Request(eyre::eyre!(Box::new(err))));
+ }
+ Err(err) => {
+ return Err(ApiError::Internal(
eyre!(err).wrap_err("failed to create payout request"),
- ),
- })?;
+ ));
+ }
+ };
// Once the Mural payout request has been created successfully,
// then we *must* commit *a* payout row into the DB, to link the Mural
diff --git a/apps/labrinth/src/queue/payouts/flow/paypal.rs b/apps/labrinth/src/queue/payouts/flow/paypal.rs
index c8e20e279a..de4b158f34 100644
--- a/apps/labrinth/src/queue/payouts/flow/paypal.rs
+++ b/apps/labrinth/src/queue/payouts/flow/paypal.rs
@@ -102,21 +102,20 @@ pub(super) async fn execute(
if let Some(venmo) = &user.venmo_handle {
("Venmo", "user_handle", venmo.clone(), venmo)
} else {
- return Err(ApiError::InvalidInput(
- "Venmo address has not been set for account!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Venmo address has not been set for account!",
+ )));
}
} else if let Some(paypal_id) = &user.paypal_id {
if let Some(paypal_country) = &user.paypal_country {
if paypal_country == "US" && method_id != "paypal_us" {
- return Err(ApiError::InvalidInput(
- "Please use the US PayPal transfer option!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Please use the US PayPal transfer option!",
+ )));
} else if paypal_country != "US" && method_id == "paypal_us" {
- return Err(ApiError::InvalidInput(
- "Please use the International PayPal transfer option!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Please use the International PayPal transfer option!",
+ )));
}
(
@@ -126,14 +125,14 @@ pub(super) async fn execute(
user.paypal_email.as_ref().unwrap_or(paypal_id),
)
} else {
- return Err(ApiError::InvalidInput(
- "Please re-link your PayPal account!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Please re-link your PayPal account!",
+ )));
}
} else {
- return Err(ApiError::InvalidInput(
- "You have not linked a PayPal account!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You have not linked a PayPal account!",
+ )));
};
let payout_req = json!({
diff --git a/apps/labrinth/src/queue/payouts/flow/tremendous.rs b/apps/labrinth/src/queue/payouts/flow/tremendous.rs
index 21f6b7100e..19b00bba95 100644
--- a/apps/labrinth/src/queue/payouts/flow/tremendous.rs
+++ b/apps/labrinth/src/queue/payouts/flow/tremendous.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use chrono::Utc;
use eyre::eyre;
use modrinth_util::decimal::Decimal2dp;
@@ -64,7 +65,8 @@ pub(super) async fn create(
"paypal" | "venmo" => {
let currency = details.currency.unwrap_or(TremendousCurrency::Usd);
let currency_code = currency.to_string();
- let usd_to_currency = usd_to_currency_for(¤cy_code)?;
+ let usd_to_currency = usd_to_currency_for(¤cy_code)
+ .wrap_api_err("executing `usd_to_currency_for`")?;
let fee = PayoutMethodFee {
// If a user withdraws $10:
@@ -127,7 +129,8 @@ pub(super) async fn create(
} else {
TremendousCurrency::Usd.to_string()
};
- let usd_to_currency = usd_to_currency_for(¤cy_code)?;
+ let usd_to_currency = usd_to_currency_for(¤cy_code)
+ .wrap_api_err("executing `usd_to_currency_for`")?;
let currency_to_usd = dec!(1) / usd_to_currency;
// no fees
@@ -190,7 +193,8 @@ pub(super) async fn execute(
pub order: Order,
}
- let user_email = get_verified_email(user)?;
+ let user_email = get_verified_email(user)
+ .wrap_api_err("fetching verified user email")?;
let order_req = json!({
"payment": {
diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs
index c237f6d624..cdc5b34cdd 100644
--- a/apps/labrinth/src/queue/payouts/mod.rs
+++ b/apps/labrinth/src/queue/payouts/mod.rs
@@ -8,6 +8,7 @@ use crate::models::payouts::{
};
use crate::models::projects::MonetizationStatus;
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::webhook::{
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
@@ -207,19 +208,17 @@ impl PayoutsQueue {
.form(&form)
.send()
.await
- .map_err(|_| {
- ApiError::Payments(
- "Error while authenticating with PayPal".to_string(),
- )
- })?
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "error while authenticating with PayPal".to_string(),
+ )?
.json()
.await
- .map_err(|_| {
- ApiError::Payments(
- "Error while authenticating with PayPal (deser error)"
- .to_string(),
- )
- })?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "error while authenticating with PayPal (deser error)"
+ .to_string(),
+ )?;
let new_creds = PayPalCredentials {
access_token: credential.access_token,
@@ -244,21 +243,23 @@ impl PayoutsQueue {
let credentials = if let Some(credentials) = read.as_ref() {
if credentials.expires < Utc::now() {
drop(read);
- self.refresh_token().await.map_err(|_| {
- ApiError::Payments(
- "Error while authenticating with PayPal".to_string(),
- )
- })?
+ self.refresh_token()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "error while authenticating with PayPal".to_string(),
+ )?
} else {
credentials.clone()
}
} else {
drop(read);
- self.refresh_token().await.map_err(|_| {
- ApiError::Payments(
- "Error while authenticating with PayPal".to_string(),
- )
- })?
+ self.refresh_token()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "error while authenticating with PayPal".to_string(),
+ )?
};
let client = reqwest::Client::new();
@@ -287,17 +288,23 @@ impl PayoutsQueue {
.body(body);
}
- let resp = request.send().await.map_err(|_| {
- ApiError::Payments("could not communicate with PayPal".to_string())
- })?;
+ let resp = request
+ .send()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "could not communicate with PayPal".to_string(),
+ )?;
let status = resp.status();
- let value = resp.json::().await.map_err(|_| {
- ApiError::Payments(
+ let value = resp
+ .json::()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
"could not retrieve PayPal response body".to_string(),
- )
- })?;
+ )?;
if !status.is_success() {
#[derive(Deserialize)]
@@ -318,27 +325,28 @@ impl PayoutsQueue {
if error.name == "INSUFFICIENT_FUNDS" {
error.message = "We're currently transferring funds to our PayPal account. Please try again in a couple days.".to_string();
}
- return Err(ApiError::Payments(format!(
+ return Err(ApiError::FailedDependency(eyre::eyre!(format!(
"error name: {}, message: {}",
error.name, error.message
- )));
+ ))));
}
if let Ok(error) =
serde_json::from_value::(value)
{
- return Err(ApiError::Payments(format!(
+ return Err(ApiError::FailedDependency(eyre::eyre!(format!(
"error name: {}, message: {}",
error.error, error.error_description
- )));
+ ))));
}
- return Err(ApiError::Payments(
- "could not retrieve PayPal error body".to_string(),
- ));
+ return Err(ApiError::FailedDependency(eyre::eyre!(
+ "could not retrieve PayPal error body",
+ )));
}
- Ok(serde_json::from_value(value)?)
+ serde_json::from_value(value)
+ .wrap_request_err("deserializing JSON data")
}
pub async fn make_tremendous_request(
@@ -359,19 +367,23 @@ impl PayoutsQueue {
request = request.json(&body);
}
- let resp = request.send().await.map_err(|_| {
- ApiError::Payments(
+ let resp = request
+ .send()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
"could not communicate with Tremendous".to_string(),
- )
- })?;
+ )?;
let status = resp.status();
- let value = resp.json::().await.map_err(|_| {
- ApiError::Payments(
+ let value = resp
+ .json::()
+ .await
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
"could not retrieve Tremendous response body".to_string(),
- )
- })?;
+ )?;
if !status.is_success()
&& let Some(obj) = value.as_object()
@@ -385,25 +397,25 @@ impl PayoutsQueue {
let err =
serde_json::from_value::(array.clone())
- .map_err(|_| {
- ApiError::Payments(
- "could not retrieve Tremendous error json body"
- .to_string(),
- )
- })?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_failed_dependency_err(
+ "could not retrieve Tremendous error json body"
+ .to_string(),
+ )?;
- return Err(ApiError::Payments(format!(
+ return Err(ApiError::FailedDependency(eyre::eyre!(format!(
"Tremendous error: {} ({:?})",
err.message, err.payload
- )));
+ ))));
}
- return Err(ApiError::Payments(
- "could not retrieve Tremendous error body".to_string(),
- ));
+ return Err(ApiError::FailedDependency(eyre::eyre!(
+ "could not retrieve Tremendous error body",
+ )));
}
- Ok(serde_json::from_value(value)?)
+ serde_json::from_value(value)
+ .wrap_request_err("deserializing JSON data")
}
pub async fn get_payout_methods(
@@ -469,13 +481,17 @@ impl PayoutsQueue {
let options = if let Some(options) = read.as_ref() {
if options.expires < Utc::now() {
drop(read);
- refresh_payout_methods(self).await?
+ refresh_payout_methods(self)
+ .await
+ .wrap_api_err("executing `refresh_payout_methods`")?
} else {
options.clone()
}
} else {
drop(read);
- refresh_payout_methods(self).await?
+ refresh_payout_methods(self)
+ .await
+ .wrap_api_err("executing `refresh_payout_methods`")?
};
Ok(options.options)
@@ -876,12 +892,18 @@ pub async fn make_aditude_request(
"interval": interval
}))
.send()
- .await?
- .error_for_status()?;
+ .await
+ .wrap_internal_err("deserializing HTTP response")?
+ .error_for_status()
+ .wrap_internal_err("deserializing HTTP response")?;
- let text = request.text().await?;
+ let text = request
+ .text()
+ .await
+ .wrap_internal_err("reading HTTP response body")?;
- let json: Vec = serde_json::from_str(&text)?;
+ let json: Vec = serde_json::from_str(&text)
+ .wrap_request_err("deserializing JSON data")?;
Ok(json)
}
@@ -900,7 +922,8 @@ pub async fn process_payout(
crate::models::payouts::PayoutStatus::InTransit.as_str(),
)
.execute(pool)
- .await?;
+ .await
+ .wrap_internal_err("writing analytics data to ClickHouse")?;
let start: DateTime = DateTime::from_naive_utc_and_offset(
(Utc::now() - Duration::days(1))
@@ -915,7 +938,8 @@ pub async fn process_payout(
start,
)
.fetch_one(pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `process_payout`")?;
if results.exists.unwrap_or(false) {
return Ok(());
@@ -966,9 +990,12 @@ pub async fn process_payout(
.bind(end.timestamp())
.fetch_one::(),
)
- .await?;
+ .await.wrap_internal_err("querying database for `process_payout`")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
struct PayoutMultipliers {
sum: u64,
@@ -1029,7 +1056,7 @@ pub async fn process_payout(
.insert(r.user_id, r.payouts_split);
async move { Ok(acc) }
})
- .await?;
+ .await.wrap_internal_err("inserting project org members into database")?;
let project_team_members = sqlx::query!(
"
@@ -1055,7 +1082,7 @@ pub async fn process_payout(
async move { Ok(acc) }
},
)
- .await?;
+ .await.wrap_internal_err("inserting project team members into database")?;
for project_id in project_ids {
let team_members: HashMap = project_team_members
@@ -1098,7 +1125,8 @@ pub async fn process_payout(
"Yesterday",
"1d",
)
- .await?;
+ .await
+ .wrap_api_err("executing `make_aditude_request`")?;
let aditude_amount: Decimal = aditude_res
.iter()
@@ -1193,9 +1221,12 @@ pub async fn process_payout(
&insert_availables[..]
)
.execute(&mut transaction)
- .await?;
+ .await.wrap_internal_err("inserting database records for `process_payout`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
@@ -1230,14 +1261,18 @@ pub async fn index_payouts_notifications(
) -> Result<(), ApiError> {
info!("Updating payout notifications");
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
payouts_values_notifications::synchronize_future_payout_values(
&mut transaction,
200,
)
- .await?;
- let items = payouts_values_notifications::PayoutsValuesNotification::unnotified_users_with_available_payouts_with_limit(&mut transaction, 200).await?;
+ .await
+ .wrap_internal_err("executing `payouts_values_notifications::synchronize_future_payout_values`")?;
+ let items = payouts_values_notifications::PayoutsValuesNotification::unnotified_users_with_available_payouts_with_limit(&mut transaction, 200).await.wrap_internal_err("executing `PayoutsValuesNotification::unnotified_users_with_available_payouts_with_limit`")?;
let payout_ref_ids = items.iter().map(|x| x.id).collect::>();
let dates_available =
@@ -1250,14 +1285,23 @@ pub async fn index_payouts_notifications(
&mut transaction,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `index_payouts_notifications`",
+ )?;
payouts_values_notifications::PayoutsValuesNotification::set_notified_many(
&payout_ref_ids,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "updating database records for `index_payouts_notifications`",
+ )?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs
index 5319350e7b..7330f80d59 100644
--- a/apps/labrinth/src/routes/analytics.rs
+++ b/apps/labrinth/src/routes/analytics.rs
@@ -9,6 +9,7 @@ use crate::queue::analytics::AnalyticsQueue;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::date::get_current_tenths_of_ms;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::http::HttpClient;
use actix_web::{HttpRequest, HttpResponse};
@@ -84,11 +85,11 @@ pub async fn page_view_ingest(
.ok();
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
- let url = Url::parse(&url_input.url).map_err(|_| {
- ApiError::InvalidInput("invalid page view URL specified!".to_string())
- })?;
- let domain = url.host_str().ok_or_else(|| {
- ApiError::InvalidInput("invalid page view URL specified!".to_string())
+ let url = Url::parse(&url_input.url)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid page view URL specified!".to_string())?;
+ let domain = url.host_str().wrap_request_err_with(|| {
+ "invalid page view URL specified!".to_string()
})?;
let url_origin = url.origin().ascii_serialization();
@@ -98,9 +99,9 @@ pub async fn page_view_ingest(
.any(|origin| origin == "*" || url_origin == *origin);
if !is_valid_url_origin {
- return Err(ApiError::InvalidInput(
- "invalid page view URL specified!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "invalid page view URL specified!",
+ )));
}
let headers = req
@@ -158,7 +159,8 @@ pub async fn page_view_ingest(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching project from database")?;
if let Some(project) = project {
view.project_id = project.inner.id.0 as u64;
@@ -209,14 +211,15 @@ pub async fn playtime_ingest(
&session_queue,
Scopes::PERFORM_ANALYTICS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let playtimes = playtime_input.0;
if playtimes.len() > 2000 {
- return Err(ApiError::InvalidInput(
- "Too much playtime entered for version!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Too much playtime entered for version!",
+ )));
}
let versions = crate::database::models::DBVersion::get_many(
@@ -224,7 +227,8 @@ pub async fn playtime_ingest(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching versions from database")?;
let headers = req.headers();
@@ -300,8 +304,9 @@ pub async fn minecraft_server_play_ingest(
let project_id = play_input.project_id;
let project = DBProject::get(&project_id.to_string(), &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
if project.components.minecraft_server.is_none() {
return Err(ApiError::Request(eyre!(
diff --git a/apps/labrinth/src/routes/debug/pprof.rs b/apps/labrinth/src/routes/debug/pprof.rs
index cf338ee386..89845bb77c 100644
--- a/apps/labrinth/src/routes/debug/pprof.rs
+++ b/apps/labrinth/src/routes/debug/pprof.rs
@@ -1,7 +1,9 @@
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use crate::util::guards::admin_key_guard;
use actix_web::{HttpResponse, get};
-use eyre::{Context, eyre};
+use eyre::eyre;
use prometheus::{IntGauge, Registry};
use std::time::Duration;
@@ -21,10 +23,12 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
#[get("/pprof/heap", guard = "admin_key_guard")]
pub async fn heap() -> Result {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
- require_profiling_activated(&prof_ctl)?;
+ require_profiling_activated(&prof_ctl)
+ .wrap_api_err("executing `require_profiling_activated`")?;
let pprof = prof_ctl
.dump_pprof()
- .map_err(|err| ApiError::InvalidInput(err.to_string()))?;
+ .map_err(|err| eyre::Report::msg(err.to_string()))
+ .wrap_request_err("processing request")?;
Ok(HttpResponse::Ok()
.content_type("application/octet-stream")
@@ -39,10 +43,12 @@ pub async fn heap() -> Result {
#[get("/pprof/heap/flamegraph", guard = "admin_key_guard")]
pub async fn flame_graph() -> Result {
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
- require_profiling_activated(&prof_ctl)?;
+ require_profiling_activated(&prof_ctl)
+ .wrap_api_err("executing `require_profiling_activated`")?;
let svg = prof_ctl
.dump_flamegraph()
- .map_err(|err| ApiError::InvalidInput(err.to_string()))?;
+ .map_err(|err| eyre::Report::msg(err.to_string()))
+ .wrap_request_err("processing request")?;
Ok(HttpResponse::Ok().content_type("image/svg+xml").body(svg))
}
@@ -53,8 +59,8 @@ fn require_profiling_activated(
if prof_ctl.activated() {
Ok(())
} else {
- Err(ApiError::InvalidInput(
- "Profiling is not activated".to_string(),
+ Err(ApiError::Request(
+ eyre::eyre!("Profiling is not activated",),
))
}
}
diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs
index dcce8286c5..0fa0196156 100644
--- a/apps/labrinth/src/routes/internal/admin.rs
+++ b/apps/labrinth/src/routes/internal/admin.rs
@@ -9,6 +9,7 @@ use crate::routes::ApiError;
use crate::search::SearchBackend;
use crate::search::incremental::consume::reindex_project_document;
use crate::util::date::get_current_tenths_of_ms;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::guards::admin_key_guard;
use crate::util::tags::valid_download_tags;
@@ -78,12 +79,11 @@ fn parse_download_meta_from_query(
for (key, value) in url.query_pairs() {
match key.as_ref() {
"mr_download_reason" => {
- meta.reason =
- Some(DownloadReason::from_str(&value).map_err(|_| {
- ApiError::Request(eyre!(
- "invalid download reason specified"
- ))
- })?);
+ meta.reason = Some(
+ DownloadReason::from_str(&value)
+ .map_err(|()| eyre::eyre!("invalid download reason"))
+ .wrap_request_err("parsing download reason")?,
+ );
}
"mr_game_version" => {
meta.game_version = Some(value.into_owned());
@@ -92,8 +92,12 @@ fn parse_download_meta_from_query(
meta.loader = Some(value.into_owned());
}
"mr_dependent_on" => {
- meta.dependent_on =
- Some(parse_download_meta_version(&value, "dependent_on")?);
+ meta.dependent_on = Some(
+ parse_download_meta_version(&value, "dependent_on")
+ .wrap_api_err(
+ "executing `parse_download_meta_version`",
+ )?,
+ );
}
_ => {}
}
@@ -122,8 +126,8 @@ async fn resolve_download_attribution_version(
crate::database::models::DBVersion::get(version_id, pool, redis)
.await
.wrap_internal_err("failed to fetch download attribution version")?
- .ok_or_else(|| {
- ApiError::Request(eyre!("invalid `{field}` version specified"))
+ .wrap_request_err_with(|| {
+ format!("invalid `{field}` version specified")
})?;
Ok(version_id.0 as u64)
@@ -185,7 +189,8 @@ pub async fn count_download(
download_body.url,
)
.fetch_optional(pool.as_ref())
- .await?
+ .await
+ .wrap_internal_err("fetching version from database")?
{
(version.id, version.mod_id)
} else if let Some(version) = sqlx::query!(
@@ -198,18 +203,19 @@ pub async fn count_download(
id_option
)
.fetch_optional(pool.as_ref())
- .await?
+ .await
+ .wrap_internal_err("fetching version from database")?
{
(version.id, version.mod_id)
} else {
- return Err(ApiError::InvalidInput(
- "Specified version does not exist!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Specified version does not exist!",
+ )));
};
- let url = url::Url::parse(&download_body.url).map_err(|_| {
- ApiError::InvalidInput("invalid download URL specified!".to_string())
- })?;
+ let url = url::Url::parse(&download_body.url)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid download URL specified!".to_string())?;
let ip = crate::util::ip::convert_to_ip_v6(&download_body.ip)
.unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped());
@@ -220,7 +226,8 @@ pub async fn count_download(
.map(Some)
.wrap_request_err("invalid download meta")?
} else {
- parse_download_meta_from_query(&url)?
+ parse_download_meta_from_query(&url)
+ .wrap_api_err("parsing download metadata from URL")?
};
if let Some(meta) = &meta {
@@ -250,7 +257,8 @@ pub async fn count_download(
meta.as_ref().and_then(|m| m.dependent_on),
"dependent_on",
)
- .await?;
+ .await
+ .wrap_api_err("executing `resolve_download_attribution_version`")?;
let download = Download {
recorded: get_current_tenths_of_ms(),
diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs
index 85f6e9ae69..8716dffba1 100644
--- a/apps/labrinth/src/routes/internal/affiliate.rs
+++ b/apps/labrinth/src/routes/internal/affiliate.rs
@@ -66,8 +66,8 @@ pub async fn ingest_click(
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
let url = ingest_click.url;
- let domain = url.host_str().ok_or_else(|| {
- ApiError::InvalidInput("invalid page view URL specified!".to_string())
+ let domain = url.host_str().wrap_request_err_with(|| {
+ "invalid page view URL specified!".to_string()
})?;
let url_origin = url.origin().ascii_serialization();
@@ -77,9 +77,9 @@ pub async fn ingest_click(
.any(|origin| origin == "*" || url_origin == *origin);
if !is_valid_url_origin {
- return Err(ApiError::InvalidInput(
- "invalid page view URL specified!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "invalid page view URL specified!",
+ )));
}
let exists = sqlx::query!(
@@ -159,7 +159,8 @@ pub async fn get_all(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
if user.role.is_admin() {
let codes = DBAffiliateCode::get_all(&**pool)
@@ -181,9 +182,9 @@ pub async fn get_all(
.collect::>();
Ok(web::Json(codes))
} else {
- Err(ApiError::CustomAuthentication(
- "You do not have permission to view affiliate codes!".to_string(),
- ))
+ Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to view affiliate codes!",
+ )))
}
}
@@ -214,16 +215,16 @@ pub async fn create(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let is_admin = creator.role.is_admin();
let is_affiliate = creator.badges.contains(Badges::AFFILIATE);
if !is_admin && !is_affiliate {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to create an affiliate code!"
- .to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to create an affiliate code!",
+ )));
}
let creator_id = DBUserId::from(creator.id);
@@ -239,19 +240,25 @@ pub async fn create(
if affiliate_id != creator_id {
let Some(_affiliate_user) =
- DBUser::get_id(affiliate_id, &**pool, &redis).await?
+ DBUser::get_id(affiliate_id, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching user from database")?
else {
- return Err(ApiError::CustomAuthentication(
- "Affiliate user not found!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "Affiliate user not found!",
+ )));
};
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let affiliate_code_id =
crate::database::models::generate_affiliate_code_id(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("generating affiliate code ID")?;
let code = DBAffiliateCode {
id: affiliate_code_id,
@@ -293,13 +300,15 @@ pub async fn get(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (affiliate_code_id,) = path.into_inner();
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
- if let Some(model) =
- DBAffiliateCode::get_by_id(affiliate_code_id, &**pool).await?
+ if let Some(model) = DBAffiliateCode::get_by_id(affiliate_code_id, &**pool)
+ .await
+ .wrap_internal_err("fetching affiliate code from database")?
{
let is_admin = user.role.is_admin();
let is_owner = model.affiliate == DBUserId::from(user.id);
@@ -307,10 +316,10 @@ pub async fn get(
if is_admin || is_owner {
Ok(web::Json(AffiliateCode::from(model, is_admin)))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -335,30 +344,33 @@ pub async fn delete(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (affiliate_code_id,) = path.into_inner();
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
- if let Some(model) =
- DBAffiliateCode::get_by_id(affiliate_code_id, &**pool).await?
+ if let Some(model) = DBAffiliateCode::get_by_id(affiliate_code_id, &**pool)
+ .await
+ .wrap_internal_err("fetching affiliate code from database")?
{
let is_admin = user.role.is_admin();
let is_owner = model.affiliate == DBUserId::from(user.id);
if is_admin || is_owner {
- let result =
- DBAffiliateCode::remove(affiliate_code_id, &**pool).await?;
+ let result = DBAffiliateCode::remove(affiliate_code_id, &**pool)
+ .await
+ .wrap_internal_err("deleting affiliate code from database")?;
if result.is_some() {
Ok(())
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -389,26 +401,28 @@ pub async fn patch(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (affiliate_code_id,) = path.into_inner();
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
let existing_code = DBAffiliateCode::get_by_id(affiliate_code_id, &**pool)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching affiliate code from database")?
+ .wrap_not_found_err("resource not found")?;
let is_admin = user.role.is_admin();
let is_owner = existing_code.affiliate == DBUserId::from(user.id);
if !is_admin && !is_owner {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
if !is_admin && !user.badges.contains(Badges::AFFILIATE) {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to update affiliate codes!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to update affiliate codes!",
+ )));
}
DBAffiliateCode::update_source_name(
diff --git a/apps/labrinth/src/routes/internal/analytics_event.rs b/apps/labrinth/src/routes/internal/analytics_event.rs
index a743703da3..5a6bec2b7e 100644
--- a/apps/labrinth/src/routes/internal/analytics_event.rs
+++ b/apps/labrinth/src/routes/internal/analytics_event.rs
@@ -56,7 +56,8 @@ pub async fn analytics_event_create(
&session_queue,
Scopes::empty(),
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.role.is_admin() {
@@ -116,7 +117,8 @@ pub async fn analytics_event_edit(
&session_queue,
Scopes::empty(),
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.role.is_admin() {
@@ -137,7 +139,7 @@ pub async fn analytics_event_edit(
.await
.wrap_internal_err("failed to update analytics event")?;
if !updated {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
DBAnalyticsEvent::clear_cache(&redis)
.await
@@ -166,7 +168,8 @@ pub async fn analytics_event_delete(
&session_queue,
Scopes::empty(),
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.role.is_admin() {
@@ -182,7 +185,7 @@ pub async fn analytics_event_delete(
.await
.wrap_internal_err("failed to delete analytics event")?;
if !deleted {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
DBAnalyticsEvent::clear_cache(&redis)
.await
diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs
index b12e7e931d..444065345c 100644
--- a/apps/labrinth/src/routes/internal/attribution.rs
+++ b/apps/labrinth/src/routes/internal/attribution.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, delete, get, patch, post, web};
use chrono::{DateTime, Utc};
use eyre::eyre;
@@ -125,7 +126,8 @@ pub async fn scan(
&session_queue,
Scopes::VERSION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let mut version_ids: Vec = body
@@ -155,7 +157,7 @@ pub async fn scan(
.wrap_internal_err("failed to fetch versions for attribution scan")?;
if versions.len() != version_ids.len() {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
let mut project_ids: Vec =
@@ -170,7 +172,8 @@ pub async fn scan(
&user,
"you do not have permission to upload versions to this project",
)
- .await?;
+ .await
+ .wrap_api_err("validating can upload versions to project")?;
}
let project_ids = project_ids.iter().map(|id| id.0).collect::>();
@@ -238,7 +241,8 @@ async fn force_scan_file(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let file_id: DBFileId = path.into_inner().into();
let file = sqlx::query!(
@@ -256,7 +260,7 @@ async fn force_scan_file(
.fetch_optional(pool.as_ref())
.await
.wrap_internal_err("failed to fetch attribution scan file")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
let mut transaction = pool.begin().await.wrap_internal_err(
"failed to begin attribution file scan transaction",
@@ -320,13 +324,15 @@ pub async fn list(
&session_queue,
Scopes::VERSION_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let requester_is_mod = user.role.is_mod();
let project = DBProject::get_id(project_id, pool.as_ref(), redis.as_ref())
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching attribution project")?
+ .wrap_not_found_err("resource not found")?;
let (team_member, organization_team_member) =
DBTeamMember::get_for_project_permissions(
&project.inner,
@@ -651,13 +657,17 @@ pub async fn update_group(
&session_queue,
Scopes::VERSION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- if !can_edit_attribution_group(pool.as_ref(), group_id, &user).await? {
- return Err(ApiError::CustomAuthentication(
- "This attribution group cannot be edited".to_string(),
- ));
+ if !can_edit_attribution_group(pool.as_ref(), group_id, &user)
+ .await
+ .wrap_api_err("checking edit attribution group")?
+ {
+ return Err(ApiError::Auth(eyre::eyre!(
+ "This attribution group cannot be edited",
+ )));
}
if matches!(
@@ -665,15 +675,15 @@ pub async fn update_group(
AttributionResolutionKind::GloballyAllowed { .. }
) && !user.role.is_mod()
{
- return Err(ApiError::CustomAuthentication(
- "Only moderators can set globally allowed attributions".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "Only moderators can set globally allowed attributions",
+ )));
}
if body.attribution.moderation_status.is_some() && !user.role.is_mod() {
- return Err(ApiError::CustomAuthentication(
- "Only moderators can set attribution moderation status".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "Only moderators can set attribution moderation status",
+ )));
}
let mut attribution = body.attribution;
@@ -698,11 +708,12 @@ pub async fn update_group(
.wrap_internal_err("failed to update attribution group")?;
if result.rows_affected() == 0 {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
clear_group_version_cache(pool.as_ref(), redis.as_ref(), &[group_id])
- .await?;
+ .await
+ .wrap_api_err("executing `clear_group_version_cache`")?;
Ok(())
}
@@ -734,7 +745,8 @@ pub async fn delete_groups(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("deleting database records for `delete_groups`")?;
let group_ids = body
.groups
@@ -772,7 +784,8 @@ pub async fn delete_all_groups(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("deleting database records for `delete_all_groups`")?;
let project_id = DBProjectId::from(body.project_id).0;
let group_ids = sqlx::query_scalar!(
@@ -849,7 +862,7 @@ async fn delete_attribution_groups(
.wrap_internal_err("failed to delete attribution groups")?;
if result.rows_affected() != group_ids.len() as u64 {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
txn.commit().await.wrap_internal_err(
@@ -891,14 +904,13 @@ pub async fn assign(
&session_queue,
Scopes::VERSION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let sha1 = body.sha1.trim().to_lowercase();
if hex_to_bytes(&sha1).is_none() {
- return Err(ApiError::InvalidInput(
- "invalid sha1 hex string".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!("invalid sha1 hex string",)));
}
let sha1_bytes = sha1.as_bytes().to_vec();
let project_id: DBProjectId = body.project_id.into();
@@ -916,7 +928,7 @@ pub async fn assign(
.fetch_optional(pool.as_ref())
.await
.wrap_internal_err("failed to fetch source attribution group")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
let target_group_exists = sqlx::query_scalar!(
"
@@ -932,21 +944,23 @@ pub async fn assign(
.wrap_internal_err("failed to check target attribution group")?;
if !target_group_exists {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
if !can_edit_attribution_group(pool.as_ref(), source_group_id, &user)
- .await?
+ .await
+ .wrap_api_err("checking edit attribution group")?
|| !can_edit_attribution_group(
pool.as_ref(),
body.target_group_id,
&user,
)
- .await?
+ .await
+ .wrap_api_err("checking edit attribution group")?
{
- return Err(ApiError::CustomAuthentication(
- "This attribution group cannot be edited".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "This attribution group cannot be edited",
+ )));
}
let mut txn = pool.begin().await.wrap_internal_err(
@@ -986,7 +1000,7 @@ pub async fn assign(
.wrap_internal_err("failed to insert assigned attribution file")?;
if result.rows_affected() == 0 {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
sqlx::query!(
@@ -1020,7 +1034,8 @@ pub async fn assign(
project_id,
&sha1_bytes,
)
- .await?;
+ .await
+ .wrap_api_err("executing `clear_project_sha1_version_cache`")?;
Ok(())
}
@@ -1052,14 +1067,13 @@ pub async fn split(
&session_queue,
Scopes::VERSION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let sha1 = body.sha1.trim().to_lowercase();
if hex_to_bytes(&sha1).is_none() {
- return Err(ApiError::InvalidInput(
- "invalid sha1 hex string".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!("invalid sha1 hex string",)));
}
let sha1_bytes = sha1.as_bytes().to_vec();
let project_id: DBProjectId = body.project_id.into();
@@ -1078,15 +1092,16 @@ pub async fn split(
.wrap_internal_err("failed to fetch attribution file to split")?;
let Some(existing) = existing else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
if !can_edit_attribution_group(pool.as_ref(), existing.group_id, &user)
- .await?
+ .await
+ .wrap_api_err("checking edit attribution group")?
{
- return Err(ApiError::CustomAuthentication(
- "This attribution group cannot be edited".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "This attribution group cannot be edited",
+ )));
}
let mut txn = pool
@@ -1138,7 +1153,8 @@ pub async fn split(
project_id,
&sha1_bytes,
)
- .await?;
+ .await
+ .wrap_api_err("executing `clear_project_sha1_version_cache`")?;
Ok(())
}
@@ -1217,7 +1233,7 @@ async fn can_edit_attribution_group(
.fetch_optional(pool)
.await
.wrap_internal_err("failed to fetch attribution group")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
ensure_can_upload_versions_to_project(
pool,
@@ -1225,7 +1241,8 @@ async fn can_edit_attribution_group(
user,
"you do not have permission to edit this attribution group",
)
- .await?;
+ .await
+ .wrap_api_err("validating can upload versions to project")?;
let attribution: Option = group
.attribution
diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs
index 82242dd145..5b4f971c96 100644
--- a/apps/labrinth/src/routes/internal/billing.rs
+++ b/apps/labrinth/src/routes/internal/billing.rs
@@ -25,6 +25,8 @@ use crate::models::users::Badges;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::anrok;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use ariadne::ids::base62_impl::{parse_base62, to_base62};
use chrono::{Duration, Utc};
@@ -79,7 +81,8 @@ pub async fn products(
let products = product_item::QueryProductWithPrices::list_purchaseable(
&**pool, &redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching query product with prices from Redis")?;
let products = products
.into_iter()
@@ -137,7 +140,8 @@ pub async fn subscriptions(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let db_subscriptions =
@@ -146,23 +150,24 @@ pub async fn subscriptions(
if user.role.is_admin() {
user_id.into()
} else {
- return Err(ApiError::InvalidInput(
- "You cannot see the subscriptions of other users!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You cannot see the subscriptions of other users!",
+ )));
}
} else {
user.id.into()
},
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
let mut subscriptions = Vec::with_capacity(db_subscriptions.len());
for subscription in db_subscriptions {
let next_charge_tax_amount =
DBCharge::get_open_subscription(subscription.id, &**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching charge from database")?
.map(|charge| charge.tax_amount);
subscriptions.push(UserSubscriptionWithNextChargeTaxAmount {
@@ -215,19 +220,25 @@ pub async fn refund_charge(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let (id,) = info.into_inner();
if !user.role.is_admin() {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to refund a subscription!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to refund a subscription!",
+ )));
}
- if let Some(charge) = DBCharge::get(id.into(), &**pool).await? {
- let refunds = DBCharge::get_children(id.into(), &**pool).await?;
+ if let Some(charge) = DBCharge::get(id.into(), &**pool)
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ {
+ let refunds = DBCharge::get_children(id.into(), &**pool)
+ .await
+ .wrap_internal_err("fetching charges from database")?;
let refunds = -refunds
.into_iter()
.filter_map(|x| match x.status {
@@ -249,16 +260,15 @@ pub async fn refund_charge(
};
if charge.status != ChargeStatus::Succeeded {
- return Err(ApiError::InvalidInput(
- "This charge cannot be refunded!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "This charge cannot be refunded!",
+ )));
}
if (refundable - refund_amount) < 0 {
- return Err(ApiError::InvalidInput(
- "You cannot refund more than the amount of the charge!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You cannot refund more than the amount of the charge!",
+ )));
}
let (id, net, anrok_result) = if refund_amount == 0 {
@@ -287,29 +297,33 @@ pub async fn refund_charge(
&payment_platform_id,
&["payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let Some(billing_address) = pi
.payment_method
.and_then(|x| x.into_object())
.and_then(|x| x.billing_details.address)
else {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"Couldn't retrieve billing address for payment method!"
.to_owned(),
- ));
+ )));
};
let tax_id = product_info_by_product_price_id(
charge.price_id,
&**pool,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find product tax info for price ID!"
- .to_owned(),
- )
+ .await
+ .wrap_api_err(
+ "executing `product_info_by_product_price_id`",
+ )?
+ .wrap_request_err_with(|| {
+ "could not find product tax info for price ID!"
+ .to_owned()
})?
.tax_identifier
.tax_processor_id;
@@ -326,10 +340,10 @@ pub async fn refund_charge(
.zip(charge.tax_transaction_version)
.zip(charge.tax_platform_accounting_time)
else {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"Charge is missing full tax information. Please wait for the original charge to be synchronized with the tax processor."
.to_owned(),
- ));
+ )));
};
let refund = stripe::Refund::create(
@@ -344,7 +358,10 @@ pub async fn refund_charge(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let anrok_txn_result = anrok_client.negate_or_create_partial_negation(
original_tax_platform_id,
@@ -373,24 +390,28 @@ pub async fn refund_charge(
Some(anrok_txn_result),
)
} else {
- return Err(ApiError::InvalidInput(
- "Charge does not have attached payment id!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Charge does not have attached payment id!",
+ )));
}
}
PaymentPlatform::None => {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"This charge was not processed via a payment platform."
.to_owned(),
- ));
+ )));
}
}
};
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- let charge_id = generate_charge_id(&mut transaction).await?;
+ let charge_id = generate_charge_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating charge ID")?;
DBCharge {
id: charge_id,
user_id: charge.user_id,
@@ -419,39 +440,47 @@ pub async fn refund_charge(
tax_platform_accounting_time: None,
}
.upsert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("generating charge ID")?;
if body.0.unprovision.unwrap_or(false)
&& let Some(subscription_id) = charge.subscription_id
{
let open_charge =
DBCharge::get_open_subscription(subscription_id, &**pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching charge from database")?;
if let Some(mut open_charge) = open_charge {
open_charge.status = ChargeStatus::Cancelled;
open_charge.due = Utc::now();
- open_charge.upsert(&mut transaction).await?;
+ open_charge
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("cancelling subscription charge")?;
}
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
if let Some(Err(error)) = anrok_result {
if let anrok::AnrokError::Conflict(m) = &error
&& m.contains("transactionExpectedVersionMismatch")
{
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"This refund has been processed on Stripe's end, but not on the tax processor's end. The tax transaction has been modified externally since its creation. \
This is likely caused by a change in nexus for the customer's jurisdiction, which lead to a new tax amount paid by the seller being calculated on the transaction. \
Manual intervention is required to verify the tax transaction on the platform's end and update the refund's tax transaction record."
.to_owned(),
- ));
+ )));
} else {
- return Err(ApiError::InvalidInput(format!(
+ return Err(ApiError::Request(eyre::eyre!(format!(
"This refund has been processed on Stripe's end, but not on the tax processor's end. An unexpected error occurred, preventing the refund transaction from being processed \
on the tax platform's end. Error: {error}"
- )));
+ ))));
}
}
}
@@ -482,80 +511,86 @@ pub async fn reprocess_charge_tax(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let (id,) = info.into_inner();
if !user.role.is_admin() {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to reprocess a tax transaction!"
- .to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to reprocess a tax transaction!",
+ )));
}
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let charge_refund = charge_item::DBCharge::get(id.into(), &mut txn)
- .await?
- .ok_or_else(|| ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ .wrap_not_found_err_with(|| "resource not found")?;
let Some(parent_charge_id) = charge_refund.parent_charge_id else {
- return Err(ApiError::InvalidInput(
- "This charge does not have a parent!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "This charge does not have a parent!",
+ )));
};
match charge_refund.tax_platform_id {
Some(_) => {
- return Err(ApiError::InvalidInput(
- "Refund charge already has a tax transaction ID!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Refund charge already has a tax transaction ID!",
+ )));
}
None => {
let charge = charge_item::DBCharge::get(parent_charge_id, &mut txn)
- .await?
- .ok_or_else(|| ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ .wrap_not_found_err_with(|| "resource not found")?;
let payment_platform_id = charge
.payment_platform_id
- .ok_or_else(|| {
- ApiError::Internal(eyre::eyre!(
- "parent charge is missing a payment platform ID"
- ))
- })?
+ .wrap_internal_err_with(
+ || "parent charge is missing a payment platform ID",
+ )?
.parse::()
- .map_err(|_| {
- ApiError::Internal(eyre::eyre!(
- "parent charge has an invalid payment platform ID."
- ))
- })?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_internal_err(
+ "parent charge has an invalid payment platform ID.",
+ )?;
let pi = stripe::PaymentIntent::retrieve(
&stripe_client,
&payment_platform_id,
&["payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
let Some(billing_address) = pi
.payment_method
.and_then(|x| x.into_object())
.and_then(|x| x.billing_details.address)
else {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"Missing billing address for payment method.".to_owned(),
- ));
+ )));
};
let tax_id =
product_info_by_product_price_id(charge.price_id, &mut txn)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find product tax info for price ID!"
- .to_owned(),
- )
+ .await
+ .wrap_api_err(
+ "executing `product_info_by_product_price_id`",
+ )?
+ .wrap_request_err_with(|| {
+ "could not find product tax info for price ID!"
+ .to_owned()
})?
.tax_identifier
.tax_processor_id;
@@ -569,22 +604,19 @@ pub async fn reprocess_charge_tax(
.zip(charge.tax_transaction_version)
.zip(charge.tax_platform_accounting_time)
else {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"Charge is missing full tax information. Please wait for the original charge to be synchronized with the tax processor."
.to_owned(),
- ));
+ )));
};
let refund_id =
- charge_refund.payment_platform_id.ok_or_else(|| {
- ApiError::Internal(eyre::eyre!(
- "Refund charge is missing a payment platform ID!"
- ))
- })?;
+ charge_refund.payment_platform_id.wrap_internal_err_with(
+ || "refund charge is missing a payment platform ID!",
+ )?;
- let refund_id =
- stripe::RefundId::from_str(&refund_id).map_err(|_| {
- ApiError::Internal(eyre::eyre!("Invalid refund ID!"))
- })?;
+ let refund_id = stripe::RefundId::from_str(&refund_id)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_internal_err("invalid refund ID!")?;
let anrok_txn_result = anrok_client
.negate_or_create_partial_negation(
@@ -621,14 +653,16 @@ pub async fn reprocess_charge_tax(
.await;
if let Err(error) = anrok_txn_result {
- return Err(ApiError::InvalidInput(format!(
+ return Err(ApiError::Request(eyre::eyre!(format!(
"There was an error processing the tax transaction: {error}. Please make sure the version has been incremented in case of an external modification."
- )));
+ ))));
}
}
}
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -678,7 +712,8 @@ pub async fn edit_subscription(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -699,21 +734,19 @@ pub async fn edit_subscription(
new_product_price.product_id,
&mut *txn,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not link new product price to product.".to_owned(),
- )
+ .await
+ .wrap_internal_err("fetching product from database")?
+ .wrap_request_err_with(|| {
+ "could not link new product price to product.".to_owned()
})?;
let current_product = product_item::DBProduct::get(
current_product_price.product_id,
&mut *txn,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not link current product price to product.".to_owned(),
- )
+ .await
+ .wrap_internal_err("fetching product from database")?
+ .wrap_request_err_with(|| {
+ "could not link current product price to product.".to_owned()
})?;
// Special case: for promoting a 'medal' subscription to 'pyro', compare the RAM. If pyro plan has:
@@ -765,11 +798,9 @@ pub async fn edit_subscription(
let current_amount = match ¤t_price.prices {
Price::OneTime { price } => *price,
Price::Recurring { intervals } => {
- *intervals.get(&duration).ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for the user's duration"
- .to_owned(),
- )
+ *intervals.get(&duration).wrap_request_err_with(|| {
+ "could not find a valid price for the user's duration"
+ .to_owned()
})?
}
};
@@ -777,11 +808,9 @@ pub async fn edit_subscription(
let amount = match &new_product_price.prices {
Price::OneTime { price } => *price,
Price::Recurring { intervals } => {
- *intervals.get(&duration).ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for the user's duration"
- .to_owned(),
- )
+ *intervals.get(&duration).wrap_request_err_with(|| {
+ "could not find a valid price for the user's duration"
+ .to_owned()
})?
}
};
@@ -791,10 +820,8 @@ pub async fn edit_subscription(
let proration = (Decimal::from(amount - current_amount) * complete)
.floor()
.to_i32()
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not convert proration to i32".to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "could not convert proration to i32".to_owned()
})?;
Ok((
@@ -815,35 +842,37 @@ pub async fn edit_subscription(
let subscription =
user_subscription_item::DBUserSubscription::get(id.into(), &**pool)
- .await?
- .ok_or_else(|| ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching user subscription from database")?
+ .wrap_not_found_err_with(|| "resource not found")?;
if subscription.user_id != user.id.into() && !user.role.is_admin() {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut open_charge = charge_item::DBCharge::get_open_subscription(
subscription.id,
&mut transaction,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find open charge for this subscription".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ .wrap_request_err_with(|| {
+ "could not find open charge for this subscription".to_string()
})?;
let current_price = product_item::DBProductPrice::get(
subscription.price_id,
&mut transaction,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find current product price".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching product price from database")?
+ .wrap_request_err_with(|| {
+ "could not find current product price".to_string()
})?;
let maybe_intent_metadata = match edit_subscription.into_inner() {
@@ -857,7 +886,10 @@ pub async fn edit_subscription(
subscription.id,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "updating users subscriptions affiliations in database",
+ )?;
open_charge.status = ChargeStatus::Cancelled;
} else {
// Forces another resubscription attempt
@@ -881,7 +913,10 @@ pub async fn edit_subscription(
subscription.id,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "executing `DBUsersSubscriptionsAffiliations::deactivate`",
+ )?;
ChargeStatus::Cancelled
} else {
ChargeStatus::Open
@@ -904,22 +939,21 @@ pub async fn edit_subscription(
product_id.into(),
&mut transaction,
)
- .await?
+ .await
+ .wrap_internal_err("fetching product prices from database")?
.into_iter()
.find(|x| x.currency_code == current_price.currency_code)
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for your currency code!"
- .to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "could not find a valid price for your currency code!"
+ .to_owned()
})?;
// The price is the same! The request likely asked to edit the product to what it already is.
if new_product_price.id == current_price.id {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"You cannot use the existing product when modifying a subscription! Modifications to only the billing interval aren't yet supported."
.to_owned(),
- ));
+ )));
}
#[derive(Serialize)]
@@ -931,9 +965,8 @@ pub async fn edit_subscription(
let currency = stripe::Currency::from_str(
¤t_price.currency_code.to_lowercase(),
)
- .map_err(|_| {
- ApiError::InvalidInput("Invalid currency code".to_string())
- })?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid currency code".to_string())?;
// The next charge is an expiring charge, so we are promoting the subscription to a paid product.
// Instead of doing a proration (since the product is likely free) we either:
@@ -944,15 +977,16 @@ pub async fn edit_subscription(
//
// ..depending on the special cases defined in `promotion_payment_requirement`.
if open_charge.status == ChargeStatus::Expiring {
- let new_region = region.ok_or_else(|| ApiError::InvalidInput("You need to specify a region when promoting an expiring charge.".to_owned()))?;
- let new_interval = interval.ok_or_else(|| ApiError::InvalidInput("You need to specify an interval when promoting an expiring charge.".to_owned()))?;
+ let new_region = region.wrap_request_err_with(|| "you need to specify a region when promoting an expiring charge.".to_owned())?;
+ let new_interval = interval.wrap_request_err_with(|| "you need to specify an interval when promoting an expiring charge.".to_owned())?;
let req = promotion_payment_requirement(
&mut transaction,
¤t_price,
&new_product_price,
)
- .await?;
+ .await
+ .wrap_api_err("executing `promotion_payment_requirement`")?;
if dry {
// Note: we aren't committing the transaction here and it will be aborted.
@@ -966,10 +1000,8 @@ pub async fn edit_subscription(
let payment_request_type =
PaymentRequestType::from_stripe_id(payment_method)
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Invalid payment method ID".to_owned(),
- )
+ .wrap_request_err_with(|| {
+ "invalid payment method ID".to_owned()
})?;
if req == PaymentRequirement::RequiresPayment {
@@ -994,14 +1026,15 @@ pub async fn edit_subscription(
attach_payment_metadata: None,
},
)
- .await?;
+ .await
+ .wrap_api_err("creating subscription charge")?;
Some(results)
} else {
/*
open_charge.status = ChargeStatus::Open;
open_charge.payment_platform = PaymentPlatform::Stripe;
- open_charge.amount = new_product_price.prices.get_interval(new_interval).ok_or_else(|| ApiError::InvalidInput("Could not find a valid price for the user's duration".to_owned()))?;
+ open_charge.amount = new_product_price.prices.get_interval(new_interval).wrap_request_err_with(|| "could not find a valid price for the user's duration".to_owned())?;
open_charge.currency_code = new_product_price.currency_code;
open_charge.subscription_interval = Some(new_interval);
open_charge.price_id = new_product_price.id;
@@ -1019,7 +1052,8 @@ pub async fn edit_subscription(
&subscription,
¤t_price,
&new_product_price,
- )?;
+ )
+ .wrap_api_err("executing `proration_amount`")?;
if dry {
// Note: we aren't committing the transaction here and it will be aborted.
@@ -1083,7 +1117,8 @@ pub async fn edit_subscription(
attach_payment_metadata: None,
},
)
- .await?;
+ .await
+ .wrap_api_err("creating subscription charge")?;
Some(results)
}
@@ -1097,25 +1132,25 @@ pub async fn edit_subscription(
interval,
..
} if region.is_some() || interval.is_some() => {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"It is not currently possible to only modify the region or interval of a subscription".to_owned(),
- ));
+ )));
}
SubscriptionEdit {
payment_method: None,
..
} => {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"A known payment method is required at this point to calculate tax information".to_owned(),
- ));
+ )));
}
_ => {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"Unexpected combination of fields in subscription PATCH request. Please either only specify `cancelled`, or specify `product` \
alongside optionally specifying a `region` and `interval`. In some cases, you may be required to provide `region` and `interval`.".to_owned(),
- ));
+ )));
}
};
@@ -1125,8 +1160,14 @@ pub async fn edit_subscription(
// At this point, if dry is true, we've already early-returned, except in
// the `cancelled` branches.
- open_charge.upsert(&mut transaction).await?;
- transaction.commit().await?;
+ open_charge
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("committing database transaction")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
}
if let Some(PaymentBootstrapResults {
@@ -1169,7 +1210,8 @@ pub async fn user_customer(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let customer_id = get_or_create_customer(
@@ -1180,9 +1222,14 @@ pub async fn user_customer(
&pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching or creating Stripe customer")?;
let customer =
- stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?;
+ stripe::Customer::retrieve(&stripe_client, &customer_id, &[])
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
Ok(HttpResponse::Ok().json(customer))
}
@@ -1214,7 +1261,8 @@ pub async fn charges(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let charges =
@@ -1223,17 +1271,17 @@ pub async fn charges(
if user.role.is_admin() {
user_id.into()
} else {
- return Err(ApiError::InvalidInput(
- "You cannot see the subscriptions of other users!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You cannot see the subscriptions of other users!",
+ )));
}
} else {
user.id.into()
},
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user charges from database")?;
Ok(HttpResponse::Ok().json(
charges
@@ -1279,7 +1327,8 @@ pub async fn add_payment_method_flow(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let customer = get_or_create_customer(
@@ -1290,7 +1339,8 @@ pub async fn add_payment_method_flow(
&pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching or creating Stripe customer")?;
let intent = SetupIntent::create(
&stripe_client,
@@ -1305,7 +1355,7 @@ pub async fn add_payment_method_flow(
..Default::default()
},
)
- .await?;
+ .await.wrap_failed_dependency_err("communicating with payment provider")?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"client_secret": intent.client_secret
@@ -1339,13 +1389,14 @@ pub async fn edit_payment_method(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let (id,) = info.into_inner();
let Ok(payment_method_id) = PaymentMethodId::from_str(&id) else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let customer = get_or_create_customer(
@@ -1356,14 +1407,16 @@ pub async fn edit_payment_method(
&pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching or creating Stripe customer")?;
let payment_method = stripe::PaymentMethod::retrieve(
&stripe_client,
&payment_method_id,
&[],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
if payment_method.customer.is_some_and(|x| x.id() == customer)
|| user.role.is_admin()
@@ -1379,11 +1432,12 @@ pub async fn edit_payment_method(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
Ok(HttpResponse::NoContent().finish())
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -1409,13 +1463,14 @@ pub async fn remove_payment_method(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let (id,) = info.into_inner();
let Ok(payment_method_id) = PaymentMethodId::from_str(&id) else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let customer = get_or_create_customer(
@@ -1426,28 +1481,35 @@ pub async fn remove_payment_method(
&pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching or creating Stripe customer")?;
let payment_method = stripe::PaymentMethod::retrieve(
&stripe_client,
&payment_method_id,
&[],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
let user_subscriptions =
user_subscription_item::DBUserSubscription::get_all_user(
user.id.into(),
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
if user_subscriptions
.iter()
.any(|x| x.status != SubscriptionStatus::Unprovisioned)
{
let customer =
- stripe::Customer::retrieve(&stripe_client, &customer, &[]).await?;
+ stripe::Customer::retrieve(&stripe_client, &customer, &[])
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
if customer
.invoice_settings
@@ -1457,10 +1519,9 @@ pub async fn remove_payment_method(
})
.unwrap_or(false)
{
- return Err(ApiError::InvalidInput(
- "You may not remove the default payment method if you have active subscriptions!"
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You may not remove the default payment method if you have active subscriptions!",
+ )));
}
}
@@ -1468,11 +1529,14 @@ pub async fn remove_payment_method(
|| user.role.is_admin()
{
stripe::PaymentMethod::detach(&stripe_client, &payment_method_id)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
Ok(HttpResponse::NoContent().finish())
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -1500,7 +1564,8 @@ pub async fn payment_methods(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if let Some(customer_id) = user
@@ -1516,7 +1581,8 @@ pub async fn payment_methods(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
Ok(HttpResponse::Ok().json(methods.data))
} else {
@@ -1560,16 +1626,15 @@ pub async fn active_servers(
.get("X-Master-Key")
.is_none_or(|it| it.as_bytes() != master_key.as_bytes())
{
- return Err(ApiError::CustomAuthentication(
- "Invalid master key".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!("Invalid master key",)));
}
let servers = user_subscription_item::DBUserSubscription::get_all_servers(
query.subscription_status,
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
let server_ids = servers
.into_iter()
@@ -1676,7 +1741,8 @@ pub async fn initiate_payment(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let payment_request = payment_request.into_inner();
@@ -1696,12 +1762,16 @@ pub async fn initiate_payment(
&**pool,
payment_request.charge,
)
- .await?,
+ .await
+ .wrap_api_err(
+ "executing `AttachedCharge::from_charge_request_type`",
+ )?,
currency: CurrencyMode::Infer,
attach_payment_metadata: payment_request.metadata,
},
)
- .await?;
+ .await
+ .wrap_api_err("executing `AttachedCharge::from_charge_request_type`")?;
match results.new_payment_intent {
Some(payment_intent) => {
@@ -1785,7 +1855,8 @@ pub async fn stripe_webhook(
crate::database::models::user_item::DBUser::get_id(
user_id, pool, redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
else {
break 'metadata;
};
@@ -1826,13 +1897,17 @@ pub async fn stripe_webhook(
crate::database::models::charge_item::DBCharge::get(
charge_id, pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching charge from database")?
{
let Some(price) = product_item::DBProductPrice::get(
charge.price_id,
pool,
)
- .await?
+ .await
+ .wrap_internal_err(
+ "fetching product price from database",
+ )?
else {
break 'metadata;
};
@@ -1841,7 +1916,8 @@ pub async fn stripe_webhook(
price.product_id,
pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching product from database")?
else {
break 'metadata;
};
@@ -1852,7 +1928,9 @@ pub async fn stripe_webhook(
Some(payment_intent_id.to_string());
charge.tax_amount = tax_amount;
charge.tax_platform_id = None;
- charge.upsert(transaction).await?;
+ charge.upsert(transaction).await.wrap_internal_err(
+ "updating payment intent charge",
+ )?;
if let Some(subscription_id) = charge.subscription_id {
let maybe_subscription =
@@ -1860,7 +1938,7 @@ pub async fn stripe_webhook(
subscription_id,
pool,
)
- .await?;
+ .await.wrap_internal_err("fetching user subscription from database")?;
let Some(mut subscription) = maybe_subscription
else {
@@ -1880,14 +1958,18 @@ pub async fn stripe_webhook(
subscription.price_id = charge.price_id;
}
ChargeType::Refund => {
- return Err(ApiError::InvalidInput(
- "Invalid charge type: Refund"
- .to_string(),
+ return Err(ApiError::Request(
+ eyre::eyre!(
+ "Invalid charge type: Refund",
+ ),
));
}
}
- subscription.upsert(transaction).await?;
+ subscription
+ .upsert(transaction)
+ .await
+ .wrap_internal_err("upserting subscription")?;
(
charge,
@@ -1914,7 +1996,10 @@ pub async fn stripe_webhook(
let Some(price) =
product_item::DBProductPrice::get(price_id, pool)
- .await?
+ .await
+ .wrap_internal_err(
+ "fetching product price from database",
+ )?
else {
break 'metadata;
};
@@ -1923,7 +2008,8 @@ pub async fn stripe_webhook(
price.product_id,
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching product from database")?;
let Some(product) = maybe_product else {
break 'metadata;
@@ -1949,7 +2035,7 @@ pub async fn stripe_webhook(
break 'metadata;
};
- let subscription = if let Some(mut subscription) = user_subscription_item::DBUserSubscription::get(subscription_id, pool).await? {
+ let subscription = if let Some(mut subscription) = user_subscription_item::DBUserSubscription::get(subscription_id, pool).await.wrap_internal_err("fetching user subscription from database")? {
subscription.status = SubscriptionStatus::Unprovisioned;
subscription.price_id = price_id;
subscription.interval = interval;
@@ -1970,7 +2056,10 @@ pub async fn stripe_webhook(
if charge_status != ChargeStatus::Failed {
subscription
.upsert(&mut *transaction)
- .await?;
+ .await
+ .wrap_internal_err(
+ "upserting subscription",
+ )?;
}
Some(subscription)
@@ -2011,7 +2100,10 @@ pub async fn stripe_webhook(
};
if charge_status != ChargeStatus::Failed {
- charge.upsert(&mut *transaction).await?;
+ charge
+ .upsert(&mut *transaction)
+ .await
+ .wrap_internal_err("upserting charge")?;
}
(charge, price, product, subscription, new_region)
@@ -2029,9 +2121,9 @@ pub async fn stripe_webhook(
});
}
- Err(ApiError::InvalidInput(
- "Webhook missing required webhook metadata!".to_string(),
- ))
+ Err(ApiError::Request(eyre::eyre!(
+ "Webhook missing required webhook metadata!",
+ )))
}
match event.type_ {
@@ -2039,7 +2131,10 @@ pub async fn stripe_webhook(
if let EventObject::PaymentIntent(payment_intent) =
event.data.object
{
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut metadata = get_payment_intent_metadata(
payment_intent.id,
@@ -2051,7 +2146,8 @@ pub async fn stripe_webhook(
ChargeStatus::Succeeded,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_api_err("fetching payment intent metadata")?;
if let Some(latest_charge) = payment_intent.latest_charge {
let charge = stripe::Charge::retrieve(
@@ -2059,7 +2155,10 @@ pub async fn stripe_webhook(
&latest_charge.id(),
&["balance_transaction"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
if let Some(balance_transaction) = charge
.balance_transaction
@@ -2070,7 +2169,8 @@ pub async fn stripe_webhook(
metadata
.charge_item
.upsert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("upserting charge")?;
}
}
@@ -2098,7 +2198,8 @@ pub async fn stripe_webhook(
as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_payment_intent_metadata`")?;
}
ProductMetadata::Pyro {
ram,
@@ -2124,11 +2225,11 @@ pub async fn stripe_webhook(
let region = metadata.new_region.clone();
if region.is_none() {
- return Err(ApiError::InvalidInput(
+ return Err(ApiError::Request(eyre::eyre!(
"We attempted to promote a subscription with type=medal, which requires specifying \
a new region to move the server to. However, no new region was present in the payment \
intent metadata.".to_owned()
- ));
+ )));
}
region
@@ -2162,8 +2263,8 @@ pub async fn stripe_webhook(
))
.header("X-Master-Key", &ENV.PYRO_API_KEY)
.send()
- .await?
- .error_for_status()?;
+ .await.wrap_internal_err("sending HTTP request")?
+ .error_for_status().wrap_internal_err("sending HTTP request")?;
client
.post(format!(
@@ -2177,8 +2278,8 @@ pub async fn stripe_webhook(
)
.json(&body)
.send()
- .await?
- .error_for_status()?;
+ .await.wrap_internal_err("deserializing HTTP response")?
+ .error_for_status().wrap_internal_err("deserializing HTTP response")?;
// As the subscription has been promoted, this is now a Pyro subscription.
// Ensure the metadata is properly updated.
@@ -2206,7 +2307,7 @@ pub async fn stripe_webhook(
None,
&**pool,
&redis,
- ).await?;
+ ).await.wrap_internal_err("fetching minecraft versions from database")?;
(
None,
@@ -2257,10 +2358,10 @@ pub async fn stripe_webhook(
})
}))
.send()
- .await?
- .error_for_status()?
+ .await.wrap_internal_err("sending HTTP request")?
+ .error_for_status().wrap_internal_err("sending HTTP request")?
.json::()
- .await?;
+ .await.wrap_internal_err("deserializing HTTP response")?;
if let Some(ref mut subscription) =
metadata.user_subscription_item
@@ -2283,17 +2384,14 @@ pub async fn stripe_webhook(
subscription.id,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching charge from database")?;
let new_price = match metadata.product_price_item.prices {
Price::OneTime { price } => price,
Price::Recurring { intervals } => {
- *intervals.get(&subscription.interval).ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for the user's country"
- .to_string(),
- )
- })?
+ *intervals.get(&subscription.interval).wrap_request_err_with(|| "could not find a valid price for the user's country"
+ .to_string())?
}
};
@@ -2333,12 +2431,19 @@ pub async fn stripe_webhook(
metadata.product_price_item.id;
charge.amount = new_price as i64;
}
- charge.upsert(&mut transaction).await?;
+ charge
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err("upserting charge")?;
} else if metadata.charge_item.status
!= ChargeStatus::Cancelled
{
let charge_id =
- generate_charge_id(&mut transaction).await?;
+ generate_charge_id(&mut transaction)
+ .await
+ .wrap_internal_err(
+ "generating charge ID",
+ )?;
DBCharge {
id: charge_id,
user_id: metadata.user_item.id,
@@ -2375,7 +2480,10 @@ pub async fn stripe_webhook(
tax_platform_accounting_time: None,
}
.upsert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err(
+ "upserting subscription charge",
+ )?;
if let Some(affiliate_code) = metadata
.payment_metadata
@@ -2390,27 +2498,42 @@ pub async fn stripe_webhook(
deactivated_at: None,
}
.insert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting subscription affiliation into database",
+ )?;
}
};
subscription.status = SubscriptionStatus::Provisioned;
- subscription.upsert(&mut transaction).await?;
+ subscription
+ .upsert(&mut transaction)
+ .await
+ .wrap_internal_err(
+ "updating affiliate code id in database",
+ )?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::user_item::DBUser::clear_caches(
&[(metadata.user_item.id, None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
}
}
EventType::PaymentIntentProcessing => {
if let EventObject::PaymentIntent(payment_intent) =
event.data.object
{
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
get_payment_intent_metadata(
payment_intent.id,
payment_intent.amount,
@@ -2421,15 +2544,22 @@ pub async fn stripe_webhook(
ChargeStatus::Processing,
&mut transaction,
)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_api_err("fetching payment intent metadata")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
}
}
EventType::PaymentIntentPaymentFailed => {
if let EventObject::PaymentIntent(payment_intent) =
event.data.object
{
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let metadata = get_payment_intent_metadata(
payment_intent.id,
@@ -2441,7 +2571,8 @@ pub async fn stripe_webhook(
ChargeStatus::Failed,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_api_err("fetching payment intent metadata")?;
if metadata.user_item.email.is_some() {
let money = rusty_money::Money::from_minor(
@@ -2474,10 +2605,16 @@ pub async fn stripe_webhook(
},
}
.insert(metadata.user_item.id, &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting payment failure notification",
+ )?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
}
}
EventType::PaymentMethodAttached => {
@@ -2491,7 +2628,10 @@ pub async fn stripe_webhook(
&customer_id,
&[],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
if customer
.invoice_settings
@@ -2512,16 +2652,19 @@ pub async fn stripe_webhook(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
}
}
}
_ => {}
}
} else {
- return Err(ApiError::InvalidInput(
- "Webhook signature validation failed!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Webhook signature validation failed!",
+ )));
}
Ok(HttpResponse::Ok().finish())
@@ -2545,7 +2688,8 @@ async fn apply_credit_many(
&subs_ids,
&mut *transaction,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
let provisioned_count = subs
.iter()
@@ -2573,18 +2717,22 @@ async fn apply_credit_many(
subscription.id,
&mut *transaction,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(format!(
- "Could not find open charge for subscription {}",
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ .wrap_request_err_with(|| {
+ format!(
+ "could not find open charge for subscription `{}`",
to_base62(subscription.id.0 as u64)
- ))
+ )
})?;
let previous_due = open_charge.due;
open_charge.due = previous_due + Duration::days(days as i64);
let next_due = open_charge.due;
- open_charge.upsert(&mut *transaction).await?;
+ open_charge
+ .upsert(&mut *transaction)
+ .await
+ .wrap_internal_err("updating next due in database")?;
credit_sub_ids.push(subscription.id);
credit_user_ids.push(subscription.user_id);
@@ -2604,7 +2752,10 @@ async fn apply_credit_many(
},
}
.insert(subscription.user_id, &mut *transaction, redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `apply_credit_many`",
+ )?;
}
}
@@ -2618,7 +2769,8 @@ async fn apply_credit_many(
&credit_next_dues,
)
.await
- .map_err(|e| ApiError::Internal(eyre::eyre!(e)))?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_internal_err("inserting subscription credits into database")?;
Ok(())
}
@@ -2668,13 +2820,14 @@ pub async fn credit(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.role.is_admin() {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to credit subscriptions!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to credit subscriptions!",
+ )));
}
let CreditRequest {
@@ -2685,18 +2838,21 @@ pub async fn credit(
} = body.into_inner();
if days <= 0 {
- return Err(ApiError::InvalidInput(
- "Days must be greater than zero".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Days must be greater than zero",
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
match target {
CreditTarget::Subscriptions { subscription_ids } => {
if subscription_ids.is_empty() {
- return Err(ApiError::InvalidInput(
- "You must specify at least one subscription id".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You must specify at least one subscription id",
+ )));
}
apply_credit_many(
&mut transaction,
@@ -2707,18 +2863,23 @@ pub async fn credit(
send_email,
message,
)
- .await?;
+ .await
+ .wrap_api_err("crediting user subscriptions")?;
}
CreditTarget::Nodes { nodes } => {
if nodes.is_empty() {
- return Err(ApiError::InvalidInput(
- "You must specify at least one node hostname".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You must specify at least one node hostname",
+ )));
}
let mut server_ids: Vec = Vec::new();
for hostname in nodes {
- let ids =
- archon_client.get_servers_by_hostname(&hostname).await?;
+ let ids = archon_client
+ .get_servers_by_hostname(&hostname)
+ .await
+ .wrap_internal_err(
+ "fetching servers by hostname from Archon",
+ )?;
server_ids.extend(ids.into_iter().map(|id| id.to_string()));
}
server_ids.dedup();
@@ -2726,11 +2887,11 @@ pub async fn credit(
&server_ids,
&mut transaction,
)
- .await?;
+ .await.wrap_internal_err("fetching user subscriptions from database")?;
if subs.is_empty() {
- return Err(ApiError::InvalidInput(
- "No subscriptions found for provided nodes".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "No subscriptions found for provided nodes",
+ )));
}
apply_credit_many(
&mut transaction,
@@ -2741,20 +2902,23 @@ pub async fn credit(
send_email,
message,
)
- .await?;
+ .await
+ .wrap_api_err("crediting server subscriptions")?;
}
CreditTarget::Region { region } => {
- let servers =
- archon_client.get_active_servers_by_region(®ion).await?;
+ let servers = archon_client
+ .get_active_servers_by_region(®ion)
+ .await
+ .wrap_internal_err("fetching servers from database")?;
let subs = user_subscription_item::DBUserSubscription::get_many_by_server_ids(
&servers.into_iter().map(|id| id.to_string()).collect::>(),
&mut transaction,
)
- .await?;
+ .await.wrap_internal_err("fetching user subscriptions from database")?;
if subs.is_empty() {
- return Err(ApiError::InvalidInput(
- "No subscriptions found for provided region".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "No subscriptions found for provided region",
+ )));
}
apply_credit_many(
&mut transaction,
@@ -2765,11 +2929,15 @@ pub async fn credit(
send_email,
message,
)
- .await?;
+ .await
+ .wrap_api_err("crediting region subscriptions")?;
}
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
}
diff --git a/apps/labrinth/src/routes/internal/billing/payments.rs b/apps/labrinth/src/routes/internal/billing/payments.rs
index 2e0f445243..e73bd778b5 100644
--- a/apps/labrinth/src/routes/internal/billing/payments.rs
+++ b/apps/labrinth/src/routes/internal/billing/payments.rs
@@ -8,6 +8,7 @@ use crate::models::v3::billing::SubscriptionStatus;
use crate::models::v3::users::User;
use crate::routes::ApiError;
use crate::util::anrok;
+use crate::util::error::{ApiContext as _, Context as _};
use xredis::RedisPool;
use crate::database::PgPool;
@@ -97,13 +98,12 @@ impl AttachedCharge {
) -> Result {
Ok(match charge_request_type {
ChargeRequestType::Existing { id } => AttachedCharge::UseExisting {
- charge: DBCharge::get(id.into(), exec).await?.ok_or_else(
- || {
- ApiError::InvalidInput(
- "Could not find charge".to_string(),
- )
- },
- )?,
+ charge: DBCharge::get(id.into(), exec)
+ .await
+ .wrap_internal_err("fetching charge from database")?
+ .wrap_request_err_with(|| {
+ "could not find charge".to_string()
+ })?,
},
ChargeRequestType::New {
product_id,
@@ -212,7 +212,8 @@ pub async fn create_or_update_payment_intent(
pg,
redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching or creating Stripe customer")?;
let mut intent_uses_confirmation_token = false;
@@ -220,15 +221,15 @@ pub async fn create_or_update_payment_intent(
PaymentSession::Interactive {
payment_request_type: PaymentRequestType::PaymentMethod { id },
} => {
- let payment_method_id =
- PaymentMethodId::from_str(id).map_err(|_| {
- ApiError::InvalidInput(
- "Invalid payment method id".to_string(),
- )
- })?;
+ let payment_method_id = PaymentMethodId::from_str(id)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid payment method id".to_string())?;
PaymentMethod::retrieve(stripe_client, &payment_method_id, &[])
- .await?
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?
}
PaymentSession::Interactive {
payment_request_type:
@@ -243,7 +244,10 @@ pub async fn create_or_update_payment_intent(
let mut confirmation: serde_json::Value = stripe_client
.get(&format!("confirmation_tokens/{token}"))
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
// We patch the JSONs to support the PaymentMethod struct
let p: json_patch::Patch = serde_json::from_value(serde_json::json!([
@@ -254,19 +258,20 @@ pub async fn create_or_update_payment_intent(
json_patch::patch(&mut confirmation, &p).unwrap();
let confirmation: ConfirmationToken =
- serde_json::from_value(confirmation)?;
+ serde_json::from_value(confirmation)
+ .wrap_request_err("deserializing JSON data")?;
- confirmation.payment_method_preview.ok_or_else(|| {
- ApiError::InvalidInput(
- "Confirmation token is missing payment method!".to_string(),
- )
- })?
+ confirmation
+ .payment_method_preview
+ .wrap_request_err_with(|| {
+ "confirmation token is missing payment method!".to_string()
+ })?
}
PaymentSession::AutomatedRenewal => {
if attached_charge.as_charge().is_none() {
- return Err(ApiError::InvalidInput(
- "Missing attached charge for automated renewal".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Missing attached charge for automated renewal",
+ )));
}
let customer = stripe::Customer::retrieve(
@@ -274,17 +279,18 @@ pub async fn create_or_update_payment_intent(
&customer_id,
&["invoice_settings.default_payment_method"],
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
customer
.invoice_settings
.and_then(|x| {
x.default_payment_method.and_then(|x| x.into_object())
})
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Customer has no default payment method!".to_string(),
- )
+ .wrap_request_err_with(|| {
+ "customer has no default payment method!".to_string()
})?
}
};
@@ -301,9 +307,8 @@ pub async fn create_or_update_payment_intent(
CurrencyMode::Infer => infer_currency_code(user_country)
.to_lowercase()
.parse::()
- .map_err(|_| {
- ApiError::InvalidInput("Invalid currency code".to_string())
- })?,
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid currency code".to_string())?,
};
let charge_data = match attached_charge {
@@ -335,7 +340,8 @@ pub async fn create_or_update_payment_intent(
Some(next_interval),
inferred_stripe_currency,
)
- .await?;
+ .await
+ .wrap_api_err("deriving proration charge data")?;
charge_data.amount = amount;
charge_data.charge_type = ChargeType::Proration;
@@ -346,29 +352,27 @@ pub async fn create_or_update_payment_intent(
interval,
current_subscription: _,
new_region: _,
- } => {
- derive_charge_data_from_product_selector(
- pg,
- user.id,
- product_id,
- Some(interval),
- inferred_stripe_currency,
- )
- .await?
- }
+ } => derive_charge_data_from_product_selector(
+ pg,
+ user.id,
+ product_id,
+ Some(interval),
+ inferred_stripe_currency,
+ )
+ .await
+ .wrap_api_err("deriving promotion charge data")?,
AttachedCharge::BaseUpon {
product_id,
interval,
- } => {
- derive_charge_data_from_product_selector(
- pg,
- user.id,
- product_id,
- interval,
- inferred_stripe_currency,
- )
- .await?
- }
+ } => derive_charge_data_from_product_selector(
+ pg,
+ user.id,
+ product_id,
+ interval,
+ inferred_stripe_currency,
+ )
+ .await
+ .wrap_api_err("deriving base charge data")?,
};
// Create an ephemeral transaction to calculate the tax amount if needed
@@ -388,23 +392,18 @@ pub async fn create_or_update_payment_intent(
charge_data.price_id.into(),
pg,
)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Missing product tax identifier for charge to continue"
- .to_owned(),
- )
+ .await
+ .wrap_internal_err("fetching product tax identifier")?
+ .wrap_request_err_with(|| {
+ "missing product tax identifier for charge to continue"
+ .to_owned()
})?;
- let address =
- payment_method.billing_details.address.clone().ok_or_else(
- || {
- ApiError::InvalidInput(
- "Missing billing details from payment method to continue"
- .to_owned(),
- )
- },
- )?;
+ let address = payment_method
+ .billing_details
+ .address
+ .clone()
+ .wrap_request_err("finding billing details on payment method")?;
let ephemeral_invoice = anrok_client
.create_ephemeral_txn(&anrok::TransactionFields {
@@ -419,7 +418,8 @@ pub async fn create_or_update_payment_intent(
customer_id: None,
customer_name: None,
})
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `create_or_update_payment_intent`")?;
ephemeral_invoice.tax_amount_to_collect
};
@@ -436,7 +436,8 @@ pub async fn create_or_update_payment_intent(
if let Some(payment_metadata) = attach_payment_metadata {
metadata.insert(
MODRINTH_PAYMENT_METADATA.to_owned(),
- serde_json::to_string(&payment_metadata)?,
+ serde_json::to_string(&payment_metadata)
+ .wrap_request_err("inserting payment metadata into database")?,
);
}
@@ -459,8 +460,13 @@ pub async fn create_or_update_payment_intent(
current_subscription,
} = attached_charge
{
- let mut transaction = pg.begin().await?;
- let charge_id = generate_charge_id(&mut transaction).await?;
+ let mut transaction = pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ let charge_id = generate_charge_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating charge ID")?;
metadata.insert(
MODRINTH_CHARGE_ID.to_owned(),
@@ -486,8 +492,13 @@ pub async fn create_or_update_payment_intent(
new_region,
} = attached_charge
{
- let mut transaction = pg.begin().await?;
- let charge_id = generate_charge_id(&mut transaction).await?;
+ let mut transaction = pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ let charge_id = generate_charge_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating charge ID")?;
metadata.insert(
MODRINTH_CHARGE_ID.to_owned(),
@@ -508,10 +519,16 @@ pub async fn create_or_update_payment_intent(
);
metadata.insert(MODRINTH_NEW_REGION.to_owned(), new_region);
} else {
- let mut transaction = pg.begin().await?;
- let charge_id = generate_charge_id(&mut transaction).await?;
- let subscription_id =
- generate_user_subscription_id(&mut transaction).await?;
+ let mut transaction = pg
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ let charge_id = generate_charge_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating charge ID")?;
+ let subscription_id = generate_user_subscription_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating charge ID")?;
metadata.insert(
MODRINTH_CHARGE_ID.to_owned(),
@@ -538,7 +555,11 @@ pub async fn create_or_update_payment_intent(
if let Some(payment_intent_id) = existing_payment_intent {
let mut update_payment_intent = stripe::UpdatePaymentIntent {
amount: Some(charge_data.amount + tax_amount),
- currency: Some(charge_data.stripe_currency_code()?),
+ currency: Some(
+ charge_data
+ .stripe_currency_code()
+ .wrap_api_err("converting charge currency for Stripe")?,
+ ),
customer: Some(customer_id),
metadata: Some(metadata),
..Default::default()
@@ -560,7 +581,10 @@ pub async fn create_or_update_payment_intent(
&payment_intent_id,
update_payment_intent,
)
- .await?;
+ .await
+ .wrap_failed_dependency_err(
+ "updating database records for `create_or_update_payment_intent`",
+ )?;
Ok(PaymentBootstrapResults {
new_payment_intent: None,
@@ -572,7 +596,9 @@ pub async fn create_or_update_payment_intent(
} else {
let mut intent = CreatePaymentIntent::new(
charge_data.amount + tax_amount,
- charge_data.stripe_currency_code()?,
+ charge_data
+ .stripe_currency_code()
+ .wrap_api_err("converting charge currency for Stripe")?,
);
intent.customer = Some(customer_id);
@@ -585,7 +611,11 @@ pub async fn create_or_update_payment_intent(
payment_session.set_payment_intent_session_options(&mut intent);
let payment_intent =
- stripe::PaymentIntent::create(stripe_client, intent).await?;
+ stripe::PaymentIntent::create(stripe_client, intent)
+ .await
+ .wrap_failed_dependency_err(
+ "communicating with payment provider",
+ )?;
Ok(PaymentBootstrapResults {
new_payment_intent: Some(payment_intent),
@@ -621,7 +651,8 @@ pub async fn get_or_create_customer(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
sqlx::query!(
"
@@ -633,13 +664,15 @@ pub async fn get_or_create_customer(
user_id.0 as i64
)
.execute(pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_or_create_customer`")?;
crate::database::models::user_item::DBUser::clear_caches(
&[(user_id.into(), None)],
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(customer.id)
}
@@ -720,9 +753,9 @@ impl ChargeData {
self.currency_code
.to_lowercase()
.parse::()
- .map_err(|_| ApiError::InvalidInput(
+ .map_err(|_| ApiError::Request(eyre::eyre!(
format!("Invalid currency code '{}': could not convert to Stripe currency", &self.currency_code)
- ))
+ )))
}
}
@@ -736,18 +769,18 @@ async fn derive_charge_data_from_product_selector(
let recommended_currency_code = stripe_currency.to_string().to_uppercase();
let product = product_item::DBProduct::get(product_id.into(), pool)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "Specified product could not be found!".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching product from database")?
+ .wrap_request_err_with(|| {
+ "specified product could not be found!".to_string()
})?;
let mut product_prices =
product_item::DBProductPrice::get_all_public_product_prices(
product.id, pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching product prices from database")?;
let price_item = if let Some(pos) = product_prices
.iter()
@@ -759,26 +792,22 @@ async fn derive_charge_data_from_product_selector(
{
product_prices.remove(pos)
} else {
- return Err(ApiError::InvalidInput(
- "Could not find a valid price for the user's country".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Could not find a valid price for the user's country",
+ )));
};
let price = match price_item.prices {
Price::OneTime { price } => price,
Price::Recurring { ref intervals } => {
- let interval = interval.ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for the user's country"
- .to_string(),
- )
+ let interval = interval.wrap_request_err_with(|| {
+ "could not find a valid price for the user's country"
+ .to_string()
})?;
- *intervals.get(&interval).ok_or_else(|| {
- ApiError::InvalidInput(
- "Could not find a valid price for the user's country"
- .to_string(),
- )
+ *intervals.get(&interval).wrap_request_err_with(|| {
+ "could not find a valid price for the user's country"
+ .to_string()
})?
}
};
@@ -791,7 +820,8 @@ async fn derive_charge_data_from_product_selector(
user_id.into(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching user subscriptions from database")?;
let user_products = product_item::DBProductPrice::get_many(
&user_subscriptions
@@ -801,15 +831,16 @@ async fn derive_charge_data_from_product_selector(
.collect::>(),
pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching product prices from database")?;
if user_products
.into_iter()
.any(|x| x.product_id == product.id)
{
- return Err(ApiError::InvalidInput(
- "You are already subscribed to this product!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You are already subscribed to this product!",
+ )));
}
}
diff --git a/apps/labrinth/src/routes/internal/billing/update_subscriptions.rs b/apps/labrinth/src/routes/internal/billing/update_subscriptions.rs
index cc003d4d22..592d11a00a 100644
--- a/apps/labrinth/src/routes/internal/billing/update_subscriptions.rs
+++ b/apps/labrinth/src/routes/internal/billing/update_subscriptions.rs
@@ -104,7 +104,10 @@ pub async fn update_many(
*region = Some(new_region);
}
- subscription.upsert(&mut txn).await?;
+ subscription
+ .upsert(&mut txn)
+ .await
+ .wrap_internal_err("updating uuid in database")?;
}
txn.commit()
diff --git a/apps/labrinth/src/routes/internal/blocked_users.rs b/apps/labrinth/src/routes/internal/blocked_users.rs
index 67b305970d..acffff0475 100644
--- a/apps/labrinth/src/routes/internal/blocked_users.rs
+++ b/apps/labrinth/src/routes/internal/blocked_users.rs
@@ -2,6 +2,7 @@ use crate::database::PgPool;
use crate::database::models::DBUserId;
use crate::database::models::blocked_user_item::DBBlockedUser;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
use crate::util::guards::admin_key_guard;
use actix_web::{get, web};
use ariadne::ids::base62_impl::parse_base62;
@@ -25,17 +26,20 @@ pub async fn block_status(
) -> Result, ApiError> {
let (user_id, target_id) = info.into_inner();
- let user_id =
- DBUserId(parse_base62(&user_id).map_err(|_| {
- ApiError::InvalidInput("invalid user_id".to_string())
- })? as i64);
- let target_id =
- DBUserId(parse_base62(&target_id).map_err(|_| {
- ApiError::InvalidInput("invalid target_id".to_string())
- })? as i64);
+ let user_id = DBUserId(
+ parse_base62(&user_id)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid user_id".to_string())? as i64,
+ );
+ let target_id = DBUserId(
+ parse_base62(&target_id)
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("invalid target_id".to_string())? as i64,
+ );
- let blocked =
- DBBlockedUser::is_blocked(user_id, target_id, &**pool).await?;
+ let blocked = DBBlockedUser::is_blocked(user_id, target_id, &**pool)
+ .await
+ .wrap_internal_err("checking whether user is blocked")?;
Ok(web::Json(BlockStatus { blocked }))
}
diff --git a/apps/labrinth/src/routes/internal/campaign.rs b/apps/labrinth/src/routes/internal/campaign.rs
index 5da7ebc414..4e317bfdbd 100644
--- a/apps/labrinth/src/routes/internal/campaign.rs
+++ b/apps/labrinth/src/routes/internal/campaign.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, get, post, web};
use base64::Engine;
use chrono::{DateTime, Duration, Utc};
@@ -158,7 +159,8 @@ pub async fn tiltify_webhook(
payouts_queue: web::Data,
body: String,
) -> Result<(), ApiError> {
- verify_tiltify_webhook_signature(&req, &body)?;
+ verify_tiltify_webhook_signature(&req, &body)
+ .wrap_api_err("executing `verify_tiltify_webhook_signature`")?;
let raw_payload = serde_json::from_str::(&body)
.wrap_internal_err_with(|| eyre!("invalid Tiltify webhook JSON"))?;
@@ -181,7 +183,9 @@ pub async fn tiltify_webhook(
.begin()
.await
.wrap_internal_err("beginning transaction")?;
- let id = generate_campaign_donation_id(&mut transaction).await?;
+ let id = generate_campaign_donation_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating campaign donation ID")?;
let mut donation = CampaignDonation {
id,
@@ -377,7 +381,8 @@ pub async fn pride_26(
total_donations_usd: response.data.total_amount_raised.value,
target_usd: response.data.goal.value,
num_donators: num_donators(&http, &access_token, campaign_id)
- .await?,
+ .await
+ .wrap_api_err("executing `num_donators`")?,
cached_at: Utc::now(),
};
diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs
index 1a5d9e55fb..ef265171d2 100644
--- a/apps/labrinth/src/routes/internal/delphi/mod.rs
+++ b/apps/labrinth/src/routes/internal/delphi/mod.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use std::{collections::HashMap, fmt::Write, time::Instant};
use xredis::RedisPool;
@@ -156,7 +157,7 @@ pub async fn ingest_report(
let report = serde_json::from_value::(report.clone())
.wrap_internal_err_with(|| {
eyre!(
- "Delphi sent a response which does not match our schema\n\n{}",
+ "received a Delphi response which does not match our schema\n\n{}",
serde_json::to_string_pretty(&report).unwrap()
)
})?;
@@ -251,7 +252,8 @@ async fn ingest_report_deserialized(
tech_review_sync::TechReviewExitReason::Resolved,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_api_err("synchronizing project technical-review state")?;
transaction
.commit()
@@ -278,7 +280,7 @@ pub async fn run(
run_parameters.file_id.0 as i64
)
.fetch_one(exec)
- .await?;
+ .await.wrap_internal_err("fetching file from database")?;
tracing::debug!(
"Running Delphi for project {}, version {}, file {}",
@@ -297,7 +299,7 @@ pub async fn run(
.send()
.await
.and_then(|res| res.error_for_status())
- .map_err(ApiError::delphi)?;
+ .wrap_internal_err("requesting a Delphi scan")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -325,7 +327,8 @@ pub async fn _run(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
run(&**pool, run_parameters.into_inner(), &http).await
}
@@ -350,12 +353,14 @@ pub async fn version(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
Ok(HttpResponse::Ok().json(
sqlx::query_scalar!("SELECT MAX(delphi_version) FROM delphi_reports")
.fetch_one(&**pool)
- .await?,
+ .await
+ .wrap_internal_err("deserializing HTTP response")?,
))
}
@@ -380,7 +385,8 @@ pub async fn issue_type_schema(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("deserializing HTTP response")?;
// This route is expected to be called often by the frontend, and Delphi is not necessarily
// built to scale beyond malware analysis, so cache the result of its quasi-constant-valued
@@ -401,10 +407,10 @@ pub async fn issue_type_schema(
.send()
.await
.and_then(|res| res.error_for_status())
- .map_err(ApiError::delphi)?
+ .wrap_internal_err("fetching the Delphi schema")?
.json::>()
.await
- .map_err(ApiError::delphi)?,
+ .wrap_internal_err("deserializing the Delphi schema")?,
Instant::now(),
))
.0,
diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
index e67c9e517e..ca80c6e9e9 100644
--- a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
+++ b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
@@ -38,6 +38,7 @@
//! to act as a single chokepoint which (correctly) syncs all the state, instead
//! of having each mutation run its own ad-hoc update logic.
+use crate::util::error::ApiContext as _;
use itertools::Itertools;
use crate::{
@@ -170,7 +171,9 @@ pub async fn sync_project_tech_review_state(
last_tech_review_message_type: row.last_tech_review_message_type,
};
- sync_one_project_tech_review_state(state, exit_reason, txn).await?;
+ sync_one_project_tech_review_state(state, exit_reason, txn)
+ .await
+ .wrap_api_err("executing `sync_one_project_tech_review_state`")?;
}
Ok(())
@@ -245,7 +248,8 @@ pub async fn sync_deleted_project_tech_review_exit(
&& should_send_exit(row.last_tech_review_message_type.as_deref())
{
insert_exit_message(thread_id, TechReviewExitReason::FileDeleted, txn)
- .await?;
+ .await
+ .wrap_api_err("executing `insert_exit_message`")?;
}
Ok(())
@@ -275,7 +279,9 @@ async fn sync_one_project_tech_review_state(
// an append-only project tech review event table where the latest
// enter/exit event is the current state. Until then, this dummy
// issue detail acts as the pending queue blocker.
- ensure_dummy_issue_detail(report_id, txn).await?;
+ ensure_dummy_issue_detail(report_id, txn)
+ .await
+ .wrap_api_err("validating dummy issue detail")?;
}
if let Some(thread_id) = state.thread_id
@@ -301,7 +307,9 @@ async fn sync_one_project_tech_review_state(
== Some(MessageBody::TechReviewEntered.as_ref())
{
if let Some(report_id) = state.report_id {
- ensure_dummy_issue_detail(report_id, txn).await?;
+ ensure_dummy_issue_detail(report_id, txn)
+ .await
+ .wrap_api_err("validating dummy issue detail")?;
}
return Ok(());
@@ -310,7 +318,9 @@ async fn sync_one_project_tech_review_state(
if let Some(thread_id) = state.thread_id
&& should_send_exit(state.last_tech_review_message_type.as_deref())
{
- insert_exit_message(thread_id, exit_reason, txn).await?;
+ insert_exit_message(thread_id, exit_reason, txn)
+ .await
+ .wrap_api_err("executing `insert_exit_message`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs
index 122edfbf79..e43eae6c54 100644
--- a/apps/labrinth/src/routes/internal/external_notifications.rs
+++ b/apps/labrinth/src/routes/internal/external_notifications.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::collections::HashMap;
use crate::auth::get_user_from_headers;
@@ -152,16 +154,23 @@ async fn create_impl(
.map(|x| DBUserId(*x))
.collect::>();
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- if !DBUser::exists_many(&user_ids, &mut txn).await? {
- return Err(ApiError::InvalidInput(
+ if !DBUser::exists_many(&user_ids, &mut txn)
+ .await
+ .wrap_internal_err("fetching users from database")?
+ {
+ return Err(ApiError::Request(eyre::eyre!(
"One of the specified users do not exist.".to_owned(),
- ));
+ )));
}
// Skip users who already have an identical notification
- let body_value = serde_json::value::to_value(&body)?;
+ let body_value = serde_json::value::to_value(&body)
+ .wrap_request_err("serializing external notification body")?;
let already_notified = sqlx::query!(
"
SELECT DISTINCT user_id
@@ -172,7 +181,8 @@ async fn create_impl(
body_value,
)
.fetch_all(&mut txn)
- .await?
+ .await
+ .wrap_internal_err("fetching already notified from database")?
.into_iter()
.map(|row| DBUserId(row.user_id))
.collect::>();
@@ -188,7 +198,8 @@ async fn create_impl(
let notification_ids = if email_strategy == EmailStrategy::Async {
notification_builder
.insert_many(notification_user_ids, &mut txn, &redis)
- .await?
+ .await
+ .wrap_internal_err("inserting notification IDs into database")?
} else {
notification_builder
.insert_many_without_delivery(
@@ -196,22 +207,31 @@ async fn create_impl(
&mut txn,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("inserting database records for `create_impl`")?
};
let notifications =
- get_site_exposed_notifications(¬ification_ids, &mut txn).await?;
+ get_site_exposed_notifications(¬ification_ids, &mut txn)
+ .await
+ .wrap_api_err("fetching site exposed notifications")?;
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
broadcast_notifications(&redis, notifications).await;
if email_strategy == EmailStrategy::Sync {
- let mut email_txn = pool.begin().await?;
+ let mut email_txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut failed = HashMap::new();
let users = DBUser::get_many_ids(&user_ids, &mut email_txn, &redis)
- .await?
+ .await
+ .wrap_internal_err("fetching users from database")?
.into_iter()
.map(|user| (user.id, user))
.collect::>();
@@ -242,10 +262,7 @@ async fn create_impl(
}
}
Err(error) => {
- if matches!(
- error,
- ApiError::SqlxDatabase(_) | ApiError::Database(_)
- ) {
+ if matches!(error, ApiError::Internal(_)) {
return Err(error);
};
failed.insert(user_id, error.into());
@@ -253,7 +270,10 @@ async fn create_impl(
};
}
- email_txn.commit().await?;
+ email_txn
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
let status = if failed
.values()
@@ -310,14 +330,20 @@ pub async fn remove(
.map(|x| DBUserId(x.0 as i64))
.collect::>();
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
DBNotification::remove_many_matching_body(
&filters, &user_ids, &mut txn, &redis,
)
- .await?;
+ .await
+ .wrap_internal_err("deleting notifications from database")?;
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -350,13 +376,14 @@ pub async fn send_custom_email(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if user.role != Role::Admin {
- return Err(ApiError::CustomAuthentication(
- "You do not have permission to send custom emails!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You do not have permission to send custom emails!",
+ )));
}
let SendEmail {
@@ -371,7 +398,10 @@ pub async fn send_custom_email(
.map(|x| DBUserId(x.0 as i64))
.collect::>();
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
NotificationBuilder {
body: NotificationBody::Custom {
@@ -381,9 +411,12 @@ pub async fn send_custom_email(
},
}
.insert_many(users, &mut txn, &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `send_custom_email`")?;
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Accepted().finish())
}
@@ -404,13 +437,15 @@ async fn get_site_exposed_notifications(
&raw_ids[..],
)
.fetch_all(&mut *txn)
- .await?
+ .await
+ .wrap_internal_err("fetching exposed IDs from database")?
.into_iter()
.map(DBNotificationId)
.collect::>();
Ok(DBNotification::get_many(&exposed_ids, txn)
- .await?
+ .await
+ .wrap_internal_err("fetching notifications from database")?
.into_iter()
.map(Notification::from)
.collect())
diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs
index fd70c60c55..4bf40c8d20 100644
--- a/apps/labrinth/src/routes/internal/flows.rs
+++ b/apps/labrinth/src/routes/internal/flows.rs
@@ -20,6 +20,7 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::internal::session::issue_session;
use crate::util::captcha::check_hcaptcha;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized;
@@ -1477,14 +1478,21 @@ pub async fn create_oauth_account(
redis: Data,
web::Json(new_account): web::Json,
) -> Result {
- new_account.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ new_account
+ .validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
- validate_account_consent(new_account.account_consent)?;
+ validate_account_consent(new_account.account_consent)
+ .wrap_api_err("validating account consent")?;
- if !check_hcaptcha(&req, &new_account.challenge).await? {
- return Err(ApiError::Turnstile);
+ if !check_hcaptcha(&req, &new_account.challenge)
+ .await
+ .wrap_api_err("checking captcha response")?
+ {
+ return Err(ApiError::Request(eyre::eyre!(
+ "captcha validation failed"
+ )));
}
let flow = DBFlow::get(&new_account.state, &redis)
@@ -1502,7 +1510,8 @@ pub async fn create_oauth_account(
};
if let Some(email) = &user.email {
- ensure_email_domain_is_allowed(email)?;
+ ensure_email_domain_is_allowed(email)
+ .wrap_api_err("validating email domain is allowed")?;
}
let mut txn = db
@@ -1520,11 +1529,16 @@ pub async fn create_oauth_account(
new_account.username,
new_account.sign_up_newsletter,
)
- .await?;
+ .await
+ .wrap_auth_err("inserting user ID into database")?;
- let session = issue_session(req, user_id, &mut txn, &redis, None).await?;
+ let session = issue_session(req, user_id, &mut txn, &redis, None)
+ .await
+ .wrap_auth_err("authenticating API request")?;
let res = crate::models::sessions::Session::from(session, true, None);
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().json(res))
}
@@ -1585,7 +1599,8 @@ pub async fn discord_community_link(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let Some(discord_id) = db_user.discord_id else {
@@ -1656,23 +1671,30 @@ pub async fn delete_auth_provider(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if user.auth_providers.is_none_or(|x| x.len() <= 1)
&& !user.has_password.unwrap_or(false)
{
- return Err(ApiError::InvalidInput(
- "You must have another authentication method added to this account!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You must have another authentication method added to this account!",
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
delete_provider
.provider
.update_user_id(user.id.into(), None, &mut transaction)
- .await?;
+ .await
+ .wrap_auth_err(
+ "updating database records for `delete_auth_provider`",
+ )?;
if delete_provider.provider != AuthProvider::PayPal {
NotificationBuilder {
@@ -1681,15 +1703,22 @@ pub async fn delete_auth_provider(
},
}
.insert(user.id.into(), &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `delete_auth_provider`",
+ )?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(
&[(user.id.into(), None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -1779,6 +1808,18 @@ impl AccountRegisterValidateError {
AccountRegisterValidateError::InvalidInput(_) => "invalid_input",
}
}
+
+ fn into_api_error(self) -> ApiError {
+ match &self {
+ Self::UsernameTaken => {
+ ApiError::Auth(eyre::eyre!(AuthenticationError::UsernameTaken))
+ }
+ Self::DuplicateEmail => {
+ ApiError::Auth(eyre::eyre!(AuthenticationError::DuplicateEmail))
+ }
+ _ => ApiError::Request(eyre::eyre!("{self}")),
+ }
+ }
}
impl actix_web::ResponseError for AccountRegisterValidateError {
@@ -1795,20 +1836,6 @@ impl actix_web::ResponseError for AccountRegisterValidateError {
}
}
-impl From for ApiError {
- fn from(value: AccountRegisterValidateError) -> Self {
- match &value {
- AccountRegisterValidateError::UsernameTaken => {
- ApiError::Authentication(AuthenticationError::UsernameTaken)
- }
- AccountRegisterValidateError::DuplicateEmail => {
- ApiError::Authentication(AuthenticationError::DuplicateEmail)
- }
- _ => ApiError::InvalidInput(value.to_string()),
- }
- }
-}
-
impl From for AccountRegisterFlow {
fn from(account: NewAccount) -> Self {
Self {
@@ -1862,9 +1889,12 @@ fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> {
}
async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> {
- ensure_email_domain_is_allowed(email)?;
+ ensure_email_domain_is_allowed(email)
+ .wrap_api_err("validating email domain is allowed")?;
- let result = check_email(email).await.map_err(ApiError::Request)?;
+ let result = check_email(email)
+ .await
+ .wrap_request_err("checking email address")?;
if matches!(
result,
@@ -1941,16 +1971,18 @@ impl ReadyAccountRegisterFlow {
) -> Result {
let register_flow = self.inner;
- let user_id =
- crate::database::models::generate_user_id(transaction).await?;
+ let user_id = crate::database::models::generate_user_id(transaction)
+ .await
+ .wrap_internal_err("generating user ID")?;
let hasher = Argon2::default();
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
let password_hash = hasher
- .hash_password(register_flow.password.as_bytes(), &salt)?
+ .hash_password(register_flow.password.as_bytes(), &salt)
+ .wrap_internal_err("hashing password")?
.to_string();
- crate::database::models::DBUser {
+ let insert_result = crate::database::models::DBUser {
id: user_id,
github_id: None,
discord_id: None,
@@ -1980,41 +2012,51 @@ impl ReadyAccountRegisterFlow {
eligibility_verified_at: Some(Utc::now()),
}
.insert(transaction)
- .await
- .map_err(|err| {
- if let sqlx::Error::Database(database_error) = &err {
- match database_error.constraint() {
- Some("username_unique" | "users_username_key") => {
- return ApiError::from(
- AccountRegisterValidateError::UsernameTaken,
- );
+ .await;
+ match insert_result {
+ Ok(()) => {}
+ Err(err) => {
+ if let sqlx::Error::Database(database_error) = &err {
+ match database_error.constraint() {
+ Some("username_unique" | "users_username_key") => {
+ return Err(
+ AccountRegisterValidateError::UsernameTaken
+ .into_api_error(),
+ );
+ }
+ Some("email_unique" | "users_email_key") => {
+ return Err(
+ AccountRegisterValidateError::DuplicateEmail
+ .into_api_error(),
+ );
+ }
+ _ => {}
}
- Some("email_unique" | "users_email_key") => {
- return ApiError::from(
- AccountRegisterValidateError::DuplicateEmail,
- );
- }
- _ => {}
}
+
+ return Err(ApiError::Internal(eyre::eyre!(
+ "inserting registered user: {err}"
+ )));
}
+ }
- ApiError::from(err)
- })?;
-
- let session =
- issue_session(req, user_id, transaction, redis, None).await?;
+ let session = issue_session(req, user_id, transaction, redis, None)
+ .await
+ .wrap_auth_err("authenticating API request")?;
let res = crate::models::sessions::Session::from(session, true, None);
- let mailbox: Mailbox = register_flow.email.parse().map_err(|_| {
- ApiError::InvalidInput("Invalid email address!".to_string())
- })?;
+ let mailbox: Mailbox = register_flow
+ .email
+ .parse()
+ .wrap_request_err("invalid email address!".to_string())?;
let flow = DBFlow::ConfirmEmail {
user_id,
confirm_email: register_flow.email.clone(),
}
.insert(Duration::hours(24), redis)
- .await?;
+ .await
+ .wrap_internal_err("storing email-verification flow in Redis")?;
email_queue
.send_one(
@@ -2023,8 +2065,10 @@ impl ReadyAccountRegisterFlow {
user_id,
mailbox,
)
- .await?
- .as_user_error()?;
+ .await
+ .wrap_api_err("sending account email")?
+ .as_user_error()
+ .wrap_api_err("validating email delivery status")?;
Ok(res)
}
@@ -2079,26 +2123,40 @@ pub async fn create_account_with_password(
) -> Result {
let new_account = new_account.into_inner();
- validate_account_consent(new_account.account_consent)?;
+ validate_account_consent(new_account.account_consent)
+ .wrap_api_err("validating account consent")?;
if !check_hcaptcha(&req, new_account.challenge.as_deref().unwrap_or(""))
- .await?
+ .await
+ .wrap_api_err("checking captcha response")?
{
- return Err(ApiError::Turnstile);
+ return Err(ApiError::Request(eyre::eyre!(
+ "captcha validation failed"
+ )));
}
- ensure_email_is_usable(&new_account.email).await?;
+ ensure_email_is_usable(&new_account.email)
+ .await
+ .wrap_api_err("validating email is usable")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let ready_flow = AccountRegisterFlow::from(new_account)
.validate(&mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err("validating ready flow")?;
let res = ready_flow
.execute(req, &mut transaction, &redis, &email)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_api_err("executing `execute`")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().json(res))
}
@@ -2129,8 +2187,13 @@ pub async fn login_password(
redis: Data,
login: web::Json,
) -> Result {
- if !check_hcaptcha(&req, &login.challenge).await? {
- return Err(ApiError::Turnstile);
+ if !check_hcaptcha(&req, &login.challenge)
+ .await
+ .wrap_api_err("checking captcha response")?
+ {
+ return Err(ApiError::Request(eyre::eyre!(
+ "captcha validation failed"
+ )));
}
let user = if let Some(user) = crate::database::models::DBUser::get(
@@ -2138,7 +2201,8 @@ pub async fn login_password(
&**pool,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
{
user
} else {
@@ -2146,12 +2210,16 @@ pub async fn login_password(
&login.username_or_email,
&**pool,
)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("fetching user from database")?;
crate::database::models::DBUser::get_id(user, &**pool, &redis)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("fetching user from database")?
};
let hasher = Argon2::default();
@@ -2161,15 +2229,19 @@ pub async fn login_password(
&PasswordHash::new(
&user
.password
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?,
- )?,
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?,
+ )
+ .wrap_internal_err("validating data")?,
)
- .map_err(|_| AuthenticationError::InvalidCredentials)?;
+ .map_err(|_| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?;
if user.totp_secret.is_some() {
let flow = DBFlow::Login2FA { user_id: user.id }
.insert(Duration::minutes(30), &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting authentication flow into database")?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"error": "2fa_required",
@@ -2177,11 +2249,19 @@ pub async fn login_password(
"flow": flow,
})))
} else {
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let session =
- issue_session(req, user.id, &mut transaction, &redis, None).await?;
+ issue_session(req, user.id, &mut transaction, &redis, None)
+ .await
+ .wrap_auth_err("authenticating API request")?;
let res = crate::models::sessions::Session::from(session, true, None);
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().json(res))
}
@@ -2285,44 +2365,60 @@ pub async fn login_2fa(
login: web::Json,
) -> Result {
let flow = DBFlow::get(&login.flow, &redis)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching login flow from Redis")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("finding login flow")?;
if let DBFlow::Login2FA { user_id } = flow {
let user =
crate::database::models::DBUser::get_id(user_id, &**pool, &redis)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("fetching user from database")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if !validate_2fa_code(
login.code.clone(),
user.totp_secret
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?,
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?,
true,
user.id,
&redis,
&pool,
&mut transaction,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
{
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
- DBFlow::remove(&login.flow, &redis).await?;
+ DBFlow::remove(&login.flow, &redis)
+ .await
+ .wrap_internal_err("removing authentication flow from Redis")?;
let session =
- issue_session(req, user_id, &mut transaction, &redis, None).await?;
+ issue_session(req, user_id, &mut transaction, &redis, None)
+ .await
+ .wrap_auth_err("authenticating API request")?;
let res = crate::models::sessions::Session::from(session, true, None);
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().json(res))
} else {
- Err(ApiError::Authentication(
+ Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ))
+ )))
}
}
@@ -2352,7 +2448,8 @@ pub async fn begin_2fa_flow(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.has_totp.unwrap_or(false) {
@@ -2364,16 +2461,17 @@ pub async fn begin_2fa_flow(
secret: encoded.to_string(),
}
.insert(Duration::minutes(30), &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `begin_2fa_flow`")?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"secret": encoded.to_string(),
"flow": flow,
})))
} else {
- Err(ApiError::InvalidInput(
- "User already has 2FA enabled on their account!".to_string(),
- ))
+ Err(ApiError::Request(eyre::eyre!(
+ "User already has 2FA enabled on their account!",
+ )))
}
}
@@ -2398,8 +2496,10 @@ pub async fn finish_2fa_flow(
session_queue: Data,
) -> Result {
let flow = DBFlow::get(&login.flow, &redis)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching 2FA initialization flow from Redis")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("finding 2FA initialization flow")?;
if let DBFlow::Initialize2FA { user_id, secret } = flow {
let user = get_user_from_headers(
@@ -2409,16 +2509,20 @@ pub async fn finish_2fa_flow(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if user.id != user_id.into() {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if !validate_2fa_code(
login.code.clone(),
@@ -2429,14 +2533,17 @@ pub async fn finish_2fa_flow(
&pool,
&mut transaction,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
{
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
- DBFlow::remove(&login.flow, &redis).await?;
+ DBFlow::remove(&login.flow, &redis)
+ .await
+ .wrap_internal_err("removing authentication flow from Redis")?;
sqlx::query!(
"
@@ -2448,7 +2555,8 @@ pub async fn finish_2fa_flow(
user_id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `finish_2fa_flow`")?;
sqlx::query!(
"
@@ -2458,7 +2566,8 @@ pub async fn finish_2fa_flow(
user_id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `finish_2fa_flow`")?;
let mut codes = Vec::new();
@@ -2479,7 +2588,8 @@ pub async fn finish_2fa_flow(
val as i64,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `finish_2fa_flow`")?;
codes.push(to_base62(val));
}
@@ -2488,22 +2598,29 @@ pub async fn finish_2fa_flow(
body: NotificationBody::TwoFactorEnabled,
}
.insert(user.id.into(), &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `finish_2fa_flow`",
+ )?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(
&[(user.id.into(), None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"backup_codes": codes,
})))
} else {
- Err(ApiError::Authentication(
+ Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ))
+ )))
}
}
@@ -2540,23 +2657,26 @@ pub async fn remove_2fa(
&session_queue,
false,
)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_auth_err("authenticating API request")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?;
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if !validate_2fa_code(
login.code.clone(),
- user.totp_secret.ok_or_else(|| {
- ApiError::InvalidInput(
- "User does not have 2FA enabled on the account!".to_string(),
- )
+ user.totp_secret.wrap_request_err_with(|| {
+ "user does not have 2FA enabled on the account!".to_string()
})?,
true,
user.id,
@@ -2564,11 +2684,12 @@ pub async fn remove_2fa(
&pool,
&mut transaction,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
{
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
sqlx::query!(
@@ -2580,7 +2701,8 @@ pub async fn remove_2fa(
user.id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `remove_2fa`")?;
sqlx::query!(
"
@@ -2590,17 +2712,23 @@ pub async fn remove_2fa(
user.id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `remove_2fa`")?;
NotificationBuilder {
body: NotificationBody::TwoFactorRemoved,
}
.insert(user.id, &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `remove_2fa`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -2631,18 +2759,27 @@ pub async fn reset_password_begin(
reset_password: web::Json,
email: web::Data,
) -> Result {
- if !check_hcaptcha(&req, &reset_password.challenge).await? {
- return Err(ApiError::Turnstile);
+ if !check_hcaptcha(&req, &reset_password.challenge)
+ .await
+ .wrap_api_err("checking captcha response")?
+ {
+ return Err(ApiError::Request(eyre::eyre!(
+ "captcha validation failed"
+ )));
}
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let user =
match crate::database::models::DBUser::get_by_case_insensitive_email(
&reset_password.username_or_email,
&mut txn,
)
- .await?[..]
+ .await
+ .wrap_internal_err("fetching user from database")?[..]
{
[] => {
// Try finding by username or ID
@@ -2651,7 +2788,8 @@ pub async fn reset_password_begin(
&mut txn,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
}
[user_id] => {
// If there is only one user with the given email, ignoring case,
@@ -2659,7 +2797,8 @@ pub async fn reset_password_begin(
crate::database::models::DBUser::get_id(
user_id, &mut txn, &redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
}
_ => {
// When several users use variations of the same email with
@@ -2671,12 +2810,16 @@ pub async fn reset_password_begin(
&reset_password.username_or_email,
&mut txn,
)
- .await?
+ .await
+ .wrap_internal_err(
+ "fetching password-reset user by email",
+ )?
{
crate::database::models::DBUser::get_id(
user_id, &mut txn, &redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching password-reset user by ID")?
} else {
None
}
@@ -2691,7 +2834,8 @@ pub async fn reset_password_begin(
{
let flow = DBFlow::ForgotPassword { user_id }
.insert(Duration::hours(24), &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting authentication flow into database")?;
if let Ok(mailbox) = user_email.unwrap_or_default().parse() {
email
@@ -2701,12 +2845,16 @@ pub async fn reset_password_begin(
user_id,
mailbox,
)
- .await?
- .as_user_error()?;
+ .await
+ .wrap_api_err("sending account email")?
+ .as_user_error()
+ .wrap_api_err("validating email delivery status")?;
}
}
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().finish())
}
@@ -2740,20 +2888,24 @@ pub async fn change_password(
session_queue: Data,
) -> Result {
let user = if let Some(flow) = &change_password.flow {
- let flow = DBFlow::get(flow, &redis).await?;
+ let flow = DBFlow::get(flow, &redis)
+ .await
+ .wrap_internal_err("fetching password-reset flow from Redis")?;
if let Some(DBFlow::ForgotPassword { user_id }) = flow {
let user = crate::database::models::DBUser::get_id(
user_id, &**pool, &redis,
)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("fetching user from database")?;
Some(user)
} else {
- return Err(ApiError::CustomAuthentication(
- "The password change flow code is invalid or has expired. Did you copy it promptly and correctly?".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "The password change flow code is invalid or has expired. Did you copy it promptly and correctly?",
+ )));
}
} else {
None
@@ -2770,33 +2922,43 @@ pub async fn change_password(
&session_queue,
false,
)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_auth_err("authenticating API request")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?;
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
if let Some(pass) = user.password.as_ref() {
- let old_password = change_password.old_password.as_ref().ok_or_else(|| {
- ApiError::CustomAuthentication(
- "You must specify the old password to change your password!".to_string(),
- )
- })?;
+ let old_password = change_password
+ .old_password
+ .as_ref()
+ .wrap_auth_err_with(|| {
+ "you must specify the old password to change your password!"
+ .to_string()
+ })?;
let hasher = Argon2::default();
- hasher.verify_password(
- old_password.as_bytes(),
- &PasswordHash::new(pass)?,
- )?;
+ hasher
+ .verify_password(
+ old_password.as_bytes(),
+ &PasswordHash::new(pass)
+ .wrap_internal_err("validating data")?,
+ )
+ .wrap_internal_err("validating data")?;
}
user
};
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let update_password = if let Some(new_password) =
&change_password.new_password
@@ -2807,21 +2969,25 @@ pub async fn change_password(
);
if score.score() < Score::Three {
- return Err(ApiError::InvalidInput(
- if let Some(feedback) =
- score.feedback().and_then(|x| x.warning())
- {
- format!("Password too weak: {feedback}")
- } else {
- "Specified password is too weak! Please improve its strength.".to_string()
- },
- ));
+ let report = score
+ .feedback()
+ .and_then(|feedback| feedback.warning())
+ .map_or_else(
+ || {
+ eyre::eyre!(
+ "Specified password is too weak! Please improve its strength."
+ )
+ },
+ |feedback| eyre::eyre!("Password too weak: {feedback}"),
+ );
+ return Err(ApiError::Request(report));
}
let hasher = Argon2::default();
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
let password_hash = hasher
- .hash_password(new_password.as_bytes(), &salt)?
+ .hash_password(new_password.as_bytes(), &salt)
+ .wrap_internal_err("hashing password")?
.to_string();
Some(password_hash)
@@ -2833,9 +2999,9 @@ pub async fn change_password(
|| user.steam_id.is_some()
|| user.discord_id.is_some())
{
- return Err(ApiError::InvalidInput(
- "You must have another authentication method added to remove password authentication!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You must have another authentication method added to remove password authentication!",
+ )));
}
None
@@ -2851,10 +3017,13 @@ pub async fn change_password(
user.id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("updating database records for `change_password`")?;
if let Some(flow) = &change_password.flow {
- DBFlow::remove(flow, &redis).await?;
+ DBFlow::remove(flow, &redis)
+ .await
+ .wrap_internal_err("removing authentication flow from Redis")?;
}
if update_password.is_some() {
@@ -2862,18 +3031,28 @@ pub async fn change_password(
body: NotificationBody::PasswordChanged,
}
.insert(user.id, &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `change_password`",
+ )?;
} else {
NotificationBuilder {
body: NotificationBody::PasswordRemoved,
}
.insert(user.id, &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `change_password`",
+ )?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::Ok().finish())
}
@@ -2907,13 +3086,15 @@ pub async fn set_email(
session_queue: Data,
stripe_client: Data,
) -> Result {
- email_address.0.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ email_address
+ .0
+ .validate()
+ .wrap_request_err("validating request")?;
- let mailbox: Mailbox = email_address.email.parse().map_err(|_| {
- ApiError::InvalidInput("Invalid email address!".to_string())
- })?;
+ let mailbox: Mailbox = email_address
+ .email
+ .parse()
+ .wrap_request_err("invalid email address!".to_string())?;
let user = get_user_from_headers(
&req,
@@ -2922,24 +3103,31 @@ pub async fn set_email(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !crate::database::models::DBUser::get_by_case_insensitive_email(
&email_address.email,
&**pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching user from database")?
.is_empty()
{
- return Err(ApiError::InvalidInput(
- "Email is already registered on Modrinth! Try 'Forgot password' in incognito to access and delete your other account.".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Email is already registered on Modrinth! Try 'Forgot password' in incognito to access and delete your other account.",
+ )));
}
- ensure_email_is_usable(&email_address.email).await?;
+ ensure_email_is_usable(&email_address.email)
+ .await
+ .wrap_api_err("validating email is usable")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
sqlx::query!(
"
@@ -2951,7 +3139,8 @@ pub async fn set_email(
user.id.0 as i64,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `set_email`")?;
if let Some(user_email) = user.email.clone() {
NotificationBuilder {
@@ -2961,7 +3150,8 @@ pub async fn set_email(
},
}
.insert(user.id.into(), &mut transaction, &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `set_email`")?;
}
if let Some(customer_id) = user
@@ -2977,7 +3167,8 @@ pub async fn set_email(
..Default::default()
},
)
- .await?;
+ .await
+ .wrap_failed_dependency_err("communicating with payment provider")?;
}
let flow = DBFlow::ConfirmEmail {
@@ -2985,7 +3176,8 @@ pub async fn set_email(
confirm_email: email_address.email.clone(),
}
.insert(Duration::hours(24), &redis)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `set_email`")?;
email
.send_one(
@@ -2994,16 +3186,22 @@ pub async fn set_email(
user.id.into(),
mailbox,
)
- .await?
- .as_user_error()?;
+ .await
+ .wrap_api_err("sending account email")?
+ .as_user_error()
+ .wrap_api_err("validating email delivery status")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(
&[(user.id.into(), None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::Ok().finish())
}
@@ -3035,14 +3233,15 @@ pub async fn resend_verify_email(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if let Some(email_address) = user.email {
if user.email_verified.unwrap_or(false) {
- return Err(ApiError::InvalidInput(
- "User email is already verified!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "User email is already verified!",
+ )));
}
let flow = DBFlow::ConfirmEmail {
@@ -3050,13 +3249,19 @@ pub async fn resend_verify_email(
confirm_email: email_address.clone(),
}
.insert(Duration::hours(24), &redis)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `resend_verify_email`",
+ )?;
- let mailbox: Mailbox = email_address.parse().map_err(|_| {
- ApiError::InvalidInput("Invalid email address!".to_string())
- })?;
+ let mailbox: Mailbox = email_address
+ .parse()
+ .wrap_request_err("invalid email address!".to_string())?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
email
.send_one(
@@ -3065,16 +3270,21 @@ pub async fn resend_verify_email(
user.id.into(),
mailbox,
)
- .await?
- .as_user_error()?;
+ .await
+ .wrap_api_err("sending account email")?
+ .as_user_error()
+ .wrap_api_err("validating email delivery status")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
} else {
- Err(ApiError::InvalidInput(
- "User does not have an email.".to_string(),
- ))
+ Err(ApiError::Request(eyre::eyre!(
+ "User does not have an email.",
+ )))
}
}
@@ -3100,7 +3310,9 @@ pub async fn verify_email(
redis: Data,
email: web::Json,
) -> Result {
- let flow = DBFlow::get(&email.flow, &redis).await?;
+ let flow = DBFlow::get(&email.flow, &redis)
+ .await
+ .wrap_internal_err("fetching email-verification flow from Redis")?;
if let Some(DBFlow::ConfirmEmail {
user_id,
@@ -3109,17 +3321,21 @@ pub async fn verify_email(
{
let user =
crate::database::models::DBUser::get_id(user_id, &**pool, &redis)
- .await?
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .await
+ .wrap_internal_err("fetching user from database")?
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("fetching user from database")?;
if user.email != Some(confirm_email) {
- return Err(ApiError::InvalidInput(
- "E-mail does not match verify email. Try re-requesting the verification link."
- .to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "E-mail does not match verify email. Try re-requesting the verification link.",
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
sqlx::query!(
"
@@ -3130,22 +3346,28 @@ pub async fn verify_email(
user.id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `verify_email`")?;
- DBFlow::remove(&email.flow, &redis).await?;
- transaction.commit().await?;
+ DBFlow::remove(&email.flow, &redis)
+ .await
+ .wrap_internal_err("removing authentication flow from Redis")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
crate::database::models::DBUser::clear_caches(
&[(user.id, None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().finish())
} else {
- Err(ApiError::InvalidInput(
- "Flow does not exist. Try re-requesting the verification link."
- .to_string(),
- ))
+ Err(ApiError::Request(eyre::eyre!(
+ "Flow does not exist. Try re-requesting the verification link.",
+ )))
}
}
@@ -3175,7 +3397,8 @@ pub async fn subscribe_newsletter(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
sqlx::query!(
@@ -3187,13 +3410,15 @@ pub async fn subscribe_newsletter(
user.id.0 as i64,
)
.execute(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `subscribe_newsletter`")?;
crate::database::models::DBUser::clear_caches(
&[(user.id.into(), None)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -3224,12 +3449,15 @@ pub async fn get_newsletter_subscription_status(
&session_queue,
Scopes::USER_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let is_subscribed = user.is_subscribed_to_newsletter
|| if let Some(email) = user.email {
- check_sendy_subscription(&email).await?
+ check_sendy_subscription(&email)
+ .await
+ .wrap_auth_err("authenticating API request")?
} else {
false
};
@@ -3275,7 +3503,8 @@ pub async fn register_passkey_start(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
// Get currently registered credentials, so an authenticator knows not to register
@@ -3377,12 +3606,14 @@ pub async fn register_passkey_finish(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- response.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ response
+ .validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
let db_user_id: DBUserId = user.id.into();
let flow = DBFlow::take_if(
@@ -3390,19 +3621,22 @@ pub async fn register_passkey_finish(
|f| matches!(f, DBFlow::RegisterPasskey { user_id, .. } if *user_id == db_user_id),
&redis,
)
- .await?;
+ .await.wrap_internal_err("executing `DBFlow::take_if`")?;
if let Some(DBFlow::RegisterPasskey { user_id, state }) = flow {
if user_id != db_user_id {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
let result = webauthn
.finish_passkey_registration(&response.credential, &state)
.wrap_request_err("failed to finish passkey registration")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let existing_passkeys =
DBPasskey::get_for_user(db_user_id, &mut transaction)
@@ -3431,9 +3665,12 @@ pub async fn register_passkey_finish(
passkey
.insert(&mut transaction)
.await
- .wrap_internal_err("Failed to create passkey object")?;
+ .wrap_internal_err("failed to create passkey object")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok((
web::Json(PasskeyResponse {
id: passkey.id.into(),
@@ -3524,7 +3761,8 @@ pub async fn authenticate_passkey_finish(
|f| matches!(f, DBFlow::AuthenticatePasskey { .. }),
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("executing `DBFlow::take_if`")?;
if let Some(DBFlow::AuthenticatePasskey { state }) = flow {
let credential_id = response.credential.get_credential_id();
@@ -3532,7 +3770,7 @@ pub async fn authenticate_passkey_finish(
DBPasskey::get_by_credential_id(credential_id, &**pool)
.await
.wrap_internal_err("failed to fetch passkey")?
- .ok_or_else(|| ApiError::Request(eyre!("passkey not found")))?;
+ .wrap_request_err_with(|| "passkey not found")?;
let mut transaction = pool
.begin()
@@ -3559,7 +3797,10 @@ pub async fn authenticate_passkey_finish(
)
.await
.wrap_internal_err("failed to invalidate user sessions")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
DBSession::clear_cache(
sessions
.into_iter()
@@ -3605,10 +3846,14 @@ pub async fn authenticate_passkey_finish(
&redis,
None,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let res = crate::models::sessions::Session::from(session, true, None);
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(web::Json(res))
} else {
Err(ApiError::Request(eyre!(
@@ -3643,7 +3888,8 @@ pub async fn list_passkeys(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let passkeys = DBPasskey::get_for_user(user.id.into(), &**pool)
@@ -3696,29 +3942,36 @@ pub async fn rename_passkey(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- body.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ body.validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
let id = DBPasskeyId(
parse_base62(&info.into_inner().0)
.wrap_request_err("invalid passkey id")? as i64,
);
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let found =
DBPasskey::rename(id, user.id.into(), &body.name, &mut transaction)
.await
.wrap_internal_err("failed to rename passkey")?;
if !found {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
}
@@ -3749,7 +4002,8 @@ pub async fn delete_passkey(
&session_queue,
Scopes::USER_AUTH_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = DBPasskeyId(
@@ -3757,16 +4011,22 @@ pub async fn delete_passkey(
.wrap_request_err("invalid passkey id")? as i64,
);
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let found =
DBPasskey::remove_for_user(id, user.id.into(), &mut transaction)
.await
.wrap_internal_err("failed to delete passkey")?;
if !found {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().finish())
}
diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs
index de25c7746d..479209bd17 100644
--- a/apps/labrinth/src/routes/internal/gdpr.rs
+++ b/apps/labrinth/src/routes/internal/gdpr.rs
@@ -3,6 +3,7 @@ use crate::database::PgPool;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, post, web};
use xredis::RedisPool;
@@ -30,46 +31,52 @@ pub async fn export(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let user_id = user.id.into();
let collection_ids =
crate::database::models::DBUser::get_collections(user_id, &**pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching users from database")?;
let collections = crate::database::models::DBCollection::get_many(
&collection_ids,
&**pool,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching collections from database")?
.into_iter()
.map(crate::models::collections::Collection::from)
.collect::>();
let follows =
crate::database::models::DBUser::get_follows(user_id, &**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching users from database")?
.into_iter()
.map(crate::models::ids::ProjectId::from)
.collect::>();
let projects =
crate::database::models::DBUser::get_projects(user_id, &**pool, &redis)
- .await?
+ .await
+ .wrap_internal_err("fetching users from database")?
.into_iter()
.map(crate::models::ids::ProjectId::from)
.collect::>();
let org_ids =
crate::database::models::DBUser::get_organizations(user_id, &**pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching users from database")?;
let orgs =
crate::database::models::organization_item::DBOrganization::get_many_ids(
&org_ids, &**pool, &redis,
)
- .await?
+ .await.wrap_internal_err("fetching organizations from database")?
.into_iter()
// TODO: add team members
.map(|x| crate::models::organizations::Organization::from(x, vec![]))
@@ -78,7 +85,7 @@ pub async fn export(
let notifs = crate::database::models::notification_item::DBNotification::get_all_user(
user_id, &**pool,
)
- .await?
+ .await.wrap_internal_err("fetching notifications from database")?
.into_iter()
.map(crate::models::notifications::Notification::from)
.collect::>();
@@ -86,7 +93,7 @@ pub async fn export(
let notifs_deliveries = crate::database::models::notifications_deliveries_item::DBNotificationDelivery::get_all_user(
user_id, &**pool,
)
- .await?
+ .await.wrap_internal_err("fetching notification deliveries from database")?
.into_iter()
.map(crate::models::notifications::NotificationDelivery::from)
.collect::>();
@@ -95,7 +102,7 @@ pub async fn export(
crate::database::models::oauth_client_item::DBOAuthClient::get_all_user_clients(
user_id, &**pool,
)
- .await?
+ .await.wrap_internal_err("fetching OAuth clients from database")?
.into_iter()
.map(crate::models::oauth_clients::OAuthClient::from)
.collect::>();
@@ -103,7 +110,7 @@ pub async fn export(
let oauth_authorizations = crate::database::models::oauth_client_authorization_item::DBOAuthClientAuthorization::get_all_for_user(
user_id, &**pool,
)
- .await?
+ .await.wrap_internal_err("fetching OAuth client authorizations from database")?
.into_iter()
.map(crate::models::oauth_clients::OAuthClientAuthorization::from)
.collect::>();
@@ -112,12 +119,13 @@ pub async fn export(
crate::database::models::pat_item::DBPersonalAccessToken::get_user_pats(
user_id, &**pool, &redis,
)
- .await?;
+ .await.wrap_internal_err("fetching personal access tokens from database")?;
let pats =
crate::database::models::pat_item::DBPersonalAccessToken::get_many_ids(
&pat_ids, &**pool, &redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching personal access tokens from database")?
.into_iter()
.map(|x| crate::models::pats::PersonalAccessToken::from(x, false))
.collect::>();
@@ -126,13 +134,15 @@ pub async fn export(
crate::database::models::payout_item::DBPayout::get_all_for_user(
user_id, &**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching payouts from database")?;
let payouts = crate::database::models::payout_item::DBPayout::get_many(
&payout_ids,
&**pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching payouts from database")?
.into_iter()
.map(crate::models::payouts::Payout::from)
.collect::>();
@@ -140,12 +150,14 @@ pub async fn export(
let report_ids = crate::database::models::user_item::DBUser::get_reports(
user_id, &**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching users from database")?;
let reports = crate::database::models::report_item::DBReport::get_many(
&report_ids,
&**pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching reports from database")?
.into_iter()
.map(crate::models::reports::Report::from)
.collect::>();
@@ -157,7 +169,7 @@ pub async fn export(
user_id.0
)
.fetch_all(pool.as_ref())
- .await?
+ .await.wrap_internal_err("fetching message IDs from database")?
.into_iter()
.map(|x| crate::database::models::ids::DBThreadMessageId(x.id))
.collect::>();
@@ -167,7 +179,8 @@ pub async fn export(
&message_ids,
&**pool,
)
- .await?
+ .await
+ .wrap_internal_err("fetching thread messages from database")?
.into_iter()
.map(|x| crate::models::threads::ThreadMessage::from(x, &user))
.collect::>();
@@ -177,7 +190,8 @@ pub async fn export(
user_id.0
)
.fetch_all(pool.as_ref())
- .await?
+ .await
+ .wrap_internal_err("fetching uploaded images IDs from database")?
.into_iter()
.map(|x| crate::database::models::ids::DBImageId(x.id))
.collect::>();
@@ -188,7 +202,8 @@ pub async fn export(
&**pool,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching images from database")?
.into_iter()
.map(crate::models::images::Image::from)
.collect::>();
@@ -197,7 +212,7 @@ pub async fn export(
crate::database::models::user_subscription_item::DBUserSubscription::get_all_user(
user_id, &**pool,
)
- .await?
+ .await.wrap_internal_err("fetching user subscriptions from database")?
.into_iter()
.map(crate::models::billing::UserSubscription::from)
.collect::>();
diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs
index 67c8f637dc..c443e19e75 100644
--- a/apps/labrinth/src/routes/internal/medal.rs
+++ b/apps/labrinth/src/routes/internal/medal.rs
@@ -1,4 +1,5 @@
use crate::database::PgPool;
+use crate::util::error::Context as _;
use actix_web::{HttpResponse, post, web};
use ariadne::ids::UserId;
use chrono::Utc;
@@ -45,10 +46,11 @@ pub async fn verify(
&username,
Offer::Medal,
)
- .await?;
+ .await
+ .wrap_internal_err("executing `RedeemalLookupFields::redeemal_status_by_username_and_offer`")?;
match maybe_fields {
- None => Err(ApiError::NotFound),
+ None => Err(ApiError::NotFound(eyre::eyre!("resource not found"))),
Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse {
user_id: fields.user_id.into(),
redeemed: fields.redeemal_status.is_some(),
@@ -71,7 +73,10 @@ pub async fn redeem(
// Check the offer hasn't been redeemed yet, then insert into the table.
// In a transaction to avoid double inserts.
- let mut txn = pool.begin().await?;
+ let mut txn = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let maybe_fields =
RedeemalLookupFields::redeemal_status_by_username_and_offer(
@@ -79,15 +84,18 @@ pub async fn redeem(
&username,
Offer::Medal,
)
- .await?;
+ .await
+ .wrap_internal_err("executing `RedeemalLookupFields::redeemal_status_by_username_and_offer`")?;
let user_id = match maybe_fields {
- None => return Err(ApiError::NotFound),
+ None => {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
+ }
Some(fields) => {
if fields.redeemal_status.is_some() {
- return Err(ApiError::Conflict(
- "User already redeemed this offer".to_string(),
- ));
+ return Err(ApiError::Conflict(eyre::eyre!(
+ "User already redeemed this offer",
+ )));
}
fields.user_id
@@ -105,9 +113,14 @@ pub async fn redeem(
n_attempts: 0,
};
- redeemal.insert(&mut txn).await?;
+ redeemal
+ .insert(&mut txn)
+ .await
+ .wrap_internal_err("inserting database records for `redeem`")?;
- txn.commit().await?;
+ txn.commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
// Immediately try to process the redeemal
if let Err(error) = try_process_user_redeemal(&pool, &redis, redeemal).await
diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs
index 493a866b59..84ae825b04 100644
--- a/apps/labrinth/src/routes/internal/moderation/external_license.rs
+++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::collections::HashMap;
use actix_web::{HttpRequest, get, patch, post, web};
@@ -99,9 +101,9 @@ impl LicenseId {
match self {
LicenseId::Number(id) => Ok(id),
LicenseId::String(id) => id.parse().map_err(|_| {
- ApiError::InvalidInput(
- "license_id must be a valid integer".to_string(),
- )
+ ApiError::Request(eyre::eyre!(
+ "license_id must be a valid integer",
+ ))
}),
}
}
@@ -143,9 +145,9 @@ fn normalize_sha1_hashes(hashes: &[String]) -> Result, ApiError> {
let hash = hash.trim().to_lowercase();
if hash.len() != 40 || !hash.chars().all(|c| c.is_ascii_hexdigit())
{
- return Err(ApiError::InvalidInput(
- "hash must be a valid SHA1 hex string".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "hash must be a valid SHA1 hex string",
+ )));
}
Ok(hash)
@@ -196,7 +198,8 @@ async fn fetch_linked_files(
license_ids,
)
.fetch_all(pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching file rows from database")?;
let mut map: HashMap> = HashMap::new();
for row in file_rows {
@@ -247,10 +250,12 @@ async fn fetch_by_hashes(
&hash_bytes,
)
.fetch_all(pool)
- .await?;
+ .await.wrap_internal_err("querying database for `fetch_by_hashes`")?;
let license_ids = rows.iter().map(|row| row.id).collect::>();
- let files_map = fetch_linked_files(pool, &license_ids).await?;
+ let files_map = fetch_linked_files(pool, &license_ids)
+ .await
+ .wrap_api_err("fetching linked files")?;
let mut results = HashMap::new();
for row in rows {
@@ -309,10 +314,13 @@ async fn fetch_by_flame_ids(
flame_ids,
)
.fetch_all(pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `fetch_by_flame_ids`")?;
let license_ids = rows.iter().map(|row| row.id).collect::>();
- let files_map = fetch_linked_files(pool, &license_ids).await?;
+ let files_map = fetch_linked_files(pool, &license_ids)
+ .await
+ .wrap_api_err("fetching linked files")?;
let mut results: HashMap> = HashMap::new();
for row in rows {
@@ -350,7 +358,8 @@ pub async fn search(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating external license search")?;
let rows = sqlx::query_as!(
LicenseRow,
@@ -381,10 +390,13 @@ pub async fn search(
body.flame_ids.as_deref(),
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `search`")?;
let license_ids: Vec = rows.iter().map(|r| r.id).collect();
- let files_map = fetch_linked_files(&pool, &license_ids).await?;
+ let files_map = fetch_linked_files(&pool, &license_ids)
+ .await
+ .wrap_api_err("fetching linked files")?;
let results = rows
.into_iter()
@@ -419,12 +431,18 @@ pub async fn lookup(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let body = body.into_inner();
- let hashes = normalize_sha1_hashes(&body.hashes)?;
- let flame_ids = fetch_by_flame_ids(&pool, &body.flame_ids).await?;
- let hashes = fetch_by_hashes(&pool, &hashes).await?;
+ let hashes = normalize_sha1_hashes(&body.hashes)
+ .wrap_api_err("executing `normalize_sha1_hashes`")?;
+ let flame_ids = fetch_by_flame_ids(&pool, &body.flame_ids)
+ .await
+ .wrap_api_err("fetching by flame ids")?;
+ let hashes = fetch_by_hashes(&pool, &hashes)
+ .await
+ .wrap_api_err("fetching by hashes")?;
Ok(web::Json(ExternalLicenseLookupResponse {
flame_ids,
@@ -453,12 +471,18 @@ pub async fn get_by_sha1(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let hashes = normalize_sha1_hashes(&[path.into_inner().0])?;
- let hash = hashes.first().ok_or(ApiError::NotFound)?;
- let mut results = fetch_by_hashes(&pool, &hashes).await?;
- let result = results.remove(hash).ok_or(ApiError::NotFound)?;
+ let hashes = normalize_sha1_hashes(&[path.into_inner().0])
+ .wrap_api_err("normalizing SHA-1 hash")?;
+ let hash = hashes.first().wrap_not_found_err("resource not found")?;
+ let mut results = fetch_by_hashes(&pool, &hashes)
+ .await
+ .wrap_api_err("fetching by hashes")?;
+ let result = results
+ .remove(hash)
+ .wrap_not_found_err("resource not found")?;
Ok(web::Json(result))
}
@@ -484,10 +508,14 @@ pub async fn get_by_sha1_bulk(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let hashes = normalize_sha1_hashes(&body.hashes)?;
- let results = fetch_by_hashes(&pool, &hashes).await?;
+ let hashes = normalize_sha1_hashes(&body.hashes)
+ .wrap_api_err("executing `normalize_sha1_hashes`")?;
+ let results = fetch_by_hashes(&pool, &hashes)
+ .await
+ .wrap_api_err("fetching by hashes")?;
Ok(web::Json(results))
}
@@ -540,16 +568,21 @@ async fn upsert_file_license(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let body = body.into_inner();
- let license_id = body.license_id.parse()?;
+ let license_id = body
+ .license_id
+ .parse()
+ .wrap_api_err("parsing external license ID")?;
if body.hashes.is_empty() {
- return Err(ApiError::InvalidInput(
- "hashes must contain at least one SHA1 hex string".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "hashes must contain at least one SHA1 hex string",
+ )));
}
- let hashes = normalize_sha1_hashes(&body.hashes)?;
+ let hashes = normalize_sha1_hashes(&body.hashes)
+ .wrap_api_err("executing `normalize_sha1_hashes`")?;
let hash_bytes = hashes
.iter()
.map(|hash| hash.as_bytes().to_vec())
@@ -557,7 +590,10 @@ async fn upsert_file_license(
let filenames = vec![None; hashes.len()];
let license_ids = vec![license_id; hashes.len()];
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let license = sqlx::query!(
r#"
@@ -579,8 +615,9 @@ async fn upsert_file_license(
license_id,
)
.fetch_optional(&mut transaction)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("fetching license from database")?
+ .wrap_not_found_err("resource not found")?;
ExternalLicense::insert_files(
&mut transaction,
@@ -589,11 +626,19 @@ async fn upsert_file_license(
&license_ids,
DBUserId(user.id.0 as i64),
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `upsert_file_license`",
+ )?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
- let files_map = fetch_linked_files(&pool, &[license_id]).await?;
+ let files_map = fetch_linked_files(&pool, &[license_id])
+ .await
+ .wrap_api_err("fetching linked files")?;
let linked_files = files_map.get(&license_id).cloned().unwrap_or_default();
Ok(web::Json(
@@ -636,7 +681,8 @@ pub async fn update_license(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let id = path.into_inner().0;
@@ -666,10 +712,13 @@ pub async fn update_license(
user.id.0 as i64,
)
.fetch_optional(&**pool)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_internal_err("querying database for `update_license`")?
+ .wrap_not_found_err("resource not found")?;
- let files_map = fetch_linked_files(&pool, &[id]).await?;
+ let files_map = fetch_linked_files(&pool, &[id])
+ .await
+ .wrap_api_err("fetching linked files")?;
let linked_files = files_map.get(&id).cloned().unwrap_or_default();
Ok(web::Json(
diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs
index aecce43cbb..b6d28c760a 100644
--- a/apps/labrinth/src/routes/internal/moderation/mod.rs
+++ b/apps/labrinth/src/routes/internal/moderation/mod.rs
@@ -8,6 +8,7 @@ use crate::models::ids::{OrganizationId, ProjectId};
use crate::models::projects::{ProjectStatus, VersionStatus};
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
use crate::queue::session::AuthQueue;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::{
auth::{check_is_moderator_from_headers, get_user_from_bearer_token},
@@ -254,7 +255,8 @@ pub async fn get_projects_internal(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let request_opts = request_opts.into_inner();
let query = normalize_optional_string(request_opts.query.as_deref());
@@ -518,7 +520,9 @@ pub async fn get_projects_internal(
row.owner_icon_url,
row.project_types,
row.external_dependencies_count,
- )? {
+ )
+ .wrap_api_err("executing `row_to_queue_project`")?
+ {
projects.push(project);
}
}
@@ -693,7 +697,9 @@ pub async fn get_projects_internal(
row.owner_icon_url,
row.project_types,
row.external_dependencies_count,
- )? {
+ )
+ .wrap_api_err("executing `row_to_queue_project`")?
+ {
projects.push(project);
}
}
@@ -731,7 +737,8 @@ pub async fn get_project_ids(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let request_opts = request_opts.into_inner();
let query = normalize_optional_string(request_opts.query.as_deref());
@@ -978,7 +985,8 @@ fn row_to_queue_project(
owner_id,
owner_name,
owner_icon_url,
- )?;
+ )
+ .wrap_api_err("executing `row_to_ownership`")?;
Ok(Some(ModerationQueueProject {
id: project_id,
@@ -1076,11 +1084,14 @@ pub async fn get_project_meta(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let project_id = info.into_inner().0;
let project =
- database::models::DBProject::get(&project_id, &**pool, &redis).await?;
+ database::models::DBProject::get(&project_id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching project from database")?;
if let Some(project) = project {
let rows = sqlx::query!(
@@ -1094,7 +1105,8 @@ pub async fn get_project_meta(
project.inner.id.0
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_project_meta`")?;
let mut merged = MissingMetadata {
identified: HashMap::new(),
@@ -1134,7 +1146,7 @@ pub async fn get_project_meta(
.collect::>()
)
.fetch_all(&**pool)
- .await?;
+ .await.wrap_internal_err("querying database for `get_project_meta`")?;
for row in rows {
if let Some(sha1) = row.sha1 {
@@ -1169,7 +1181,8 @@ pub async fn get_project_meta(
&check_flames,
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `get_project_meta`")?;
for row in rows {
if let Some(sha1) = merged
@@ -1192,7 +1205,7 @@ pub async fn get_project_meta(
Ok(web::Json(merged))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -1234,9 +1247,13 @@ pub async fn set_project_meta(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let mut licenses = Vec::new();
let mut file_hashes = Vec::new();
@@ -1287,7 +1304,8 @@ pub async fn set_project_meta(
&licenses,
user_id,
)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `set_project_meta`")?;
moderation_external_item::ExternalLicense::insert_files(
&mut transaction,
@@ -1299,9 +1317,13 @@ pub async fn set_project_meta(
&file_license_ids,
user_id,
)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `set_project_meta`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
@@ -1331,18 +1353,23 @@ pub async fn acquire_lock(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let project_id_str = path.into_inner().0;
let project =
database::models::DBProject::get(&project_id_str, &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
let db_project_id = project.inner.id;
let db_user_id = database::models::DBUserId::from(user.id);
- match DBModerationLock::acquire(db_project_id, db_user_id, &pool).await? {
+ match DBModerationLock::acquire(db_project_id, db_user_id, &pool)
+ .await
+ .wrap_internal_err("executing `DBModerationLock::acquire`")?
+ {
Ok(()) => Ok(web::Json(LockAcquireResponse {
success: true,
is_own_lock: true,
@@ -1390,18 +1417,22 @@ pub async fn override_lock(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let project_id_str = path.into_inner().0;
let project =
database::models::DBProject::get(&project_id_str, &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
let db_project_id = project.inner.id;
let db_user_id = database::models::DBUserId::from(user.id);
- DBModerationLock::force_acquire(db_project_id, db_user_id, &pool).await?;
+ DBModerationLock::force_acquire(db_project_id, db_user_id, &pool)
+ .await
+ .wrap_internal_err("executing `DBModerationLock::force_acquire`")?;
Ok(web::Json(LockAcquireResponse {
success: true,
@@ -1437,18 +1468,23 @@ pub async fn get_lock_status(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let project_id_str = path.into_inner().0;
let project =
database::models::DBProject::get(&project_id_str, &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
let db_project_id = project.inner.id;
let db_user_id = database::models::DBUserId::from(user.id);
- match DBModerationLock::get_with_user(db_project_id, &pool).await? {
+ match DBModerationLock::get_with_user(db_project_id, &pool)
+ .await
+ .wrap_internal_err("fetching moderation lock from database")?
+ {
Some(lock) => {
let is_own_lock = lock.moderator_id == db_user_id;
Ok(web::Json(LockStatusResponse {
@@ -1499,19 +1535,22 @@ pub async fn release_lock(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let project_id_str = path.into_inner().0;
let project =
database::models::DBProject::get(&project_id_str, &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
let db_project_id = project.inner.id;
let db_user_id = database::models::DBUserId::from(user.id);
- let released =
- DBModerationLock::release(db_project_id, db_user_id, &pool).await?;
+ let released = DBModerationLock::release(db_project_id, db_user_id, &pool)
+ .await
+ .wrap_internal_err("executing `DBModerationLock::release`")?;
let _ = DBModerationLock::cleanup_expired(&pool).await;
@@ -1547,9 +1586,9 @@ pub async fn release_lock_beacon(
) -> Result, ApiError> {
let token = body.trim();
if token.is_empty() {
- return Err(ApiError::InvalidInput(
- "missing token in request body".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "missing token in request body",
+ )));
}
let token = token.strip_prefix("Bearer ").unwrap_or(token).trim();
@@ -1561,30 +1600,33 @@ pub async fn release_lock_beacon(
&session_queue,
false,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
if !scopes.contains(Scopes::PROJECT_WRITE) {
- return Err(ApiError::CustomAuthentication(
- "token is missing required scopes".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "token is missing required scopes",
+ )));
}
if !user.role.is_mod() {
- return Err(ApiError::CustomAuthentication(
- "only moderators may release moderation locks".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "only moderators may release moderation locks",
+ )));
}
let project_id_str = path.into_inner().0;
let project =
database::models::DBProject::get(&project_id_str, &**pool, &redis)
- .await?
- .ok_or(ApiError::NotFound)?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_not_found_err("resource not found")?;
let db_project_id = project.inner.id;
let db_user_id = database::models::DBUserId::from(user.id);
- let released =
- DBModerationLock::release(db_project_id, db_user_id, &pool).await?;
+ let released = DBModerationLock::release(db_project_id, db_user_id, &pool)
+ .await
+ .wrap_internal_err("executing `DBModerationLock::release`")?;
let _ = DBModerationLock::cleanup_expired(&pool).await;
@@ -1614,16 +1656,19 @@ pub async fn delete_all_locks(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
if !user.role.is_admin() {
- return Err(ApiError::CustomAuthentication(
- "You must be an admin to delete all locks".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You must be an admin to delete all locks",
+ )));
}
- let deleted_count = DBModerationLock::delete_all(&pool).await?;
+ let deleted_count = DBModerationLock::delete_all(&pool)
+ .await
+ .wrap_internal_err("deleting moderation locks from database")?;
Ok(web::Json(DeleteAllLocksResponse { deleted_count }))
}
diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs
index ea694627eb..c55d8fdbc7 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use std::{collections::HashMap, fmt};
use xredis::RedisPool;
@@ -225,7 +226,8 @@ pub async fn get_issue(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (issue_id,) = path.into_inner();
let row = sqlx::query!(
@@ -261,7 +263,7 @@ pub async fn get_issue(
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to fetch issue from database")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
Ok(web::Json(row.data.0))
}
@@ -288,7 +290,8 @@ pub async fn get_report(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (report_id,) = path.into_inner();
@@ -348,7 +351,7 @@ pub async fn get_report(
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to fetch report from database")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
Ok(web::Json(row.data.0))
}
@@ -703,7 +706,8 @@ pub async fn search_projects(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let sort_by = search_req.sort_by.to_string();
let limit = search_req.limit.max(50);
@@ -819,8 +823,9 @@ pub async fn search_projects(
thread_ids.push(row.thread_id);
}
- let project_reports =
- fetch_project_reports(&project_ids, &pool, &redis).await?;
+ let project_reports = fetch_project_reports(&project_ids, &pool, &redis)
+ .await
+ .wrap_api_err("fetching project reports")?;
let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis)
.await
@@ -913,7 +918,8 @@ pub async fn get_project_report(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (project_id,) = path.into_inner();
let db_project_id = DBProjectId::from(project_id);
@@ -929,10 +935,12 @@ pub async fn get_project_report(
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to fetch thread")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
let project_reports =
- fetch_project_reports(&[db_project_id], &pool, &redis).await?;
+ fetch_project_reports(&[db_project_id], &pool, &redis)
+ .await
+ .wrap_api_err("fetching project reports")?;
let project_report = project_reports.into_iter().next();
@@ -966,7 +974,7 @@ pub async fn get_project_report(
let thread = threads
.get(&row.thread_id.into())
.cloned()
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
Ok(web::Json(ProjectReportResponse {
project_report,
@@ -988,8 +996,8 @@ pub struct SubmitReport {
///
/// Before this is called, all issues for this project's reports must have been
/// marked as either safe or unsafe. Otherwise, this will error with
-/// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which
-/// are still unmarked.
+/// A request error is returned with the issue detail IDs which are still
+/// unmarked.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
@@ -1013,7 +1021,8 @@ pub async fn submit_report(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let (project_id,) = path.into_inner();
let project_id = DBProjectId::from(project_id);
@@ -1045,14 +1054,13 @@ pub async fn submit_report(
.wrap_internal_err("failed to fetch pending issues")?;
if !pending_issue_details.is_empty() {
- return Err(ApiError::TechReviewDetailsWithNoVerdict {
- details: pending_issue_details
- .into_iter()
- .map(|record| {
- DelphiReportIssueDetailsId(record.issue_detail_id)
- })
- .collect(),
- });
+ let details = pending_issue_details
+ .into_iter()
+ .map(|record| DelphiReportIssueDetailsId(record.issue_detail_id))
+ .collect_vec();
+ return Err(ApiError::Request(eyre::eyre!(
+ "report still has issue details with no verdict: {details:?}"
+ )));
}
sqlx::query!(
@@ -1171,7 +1179,10 @@ pub async fn submit_report(
None,
None,
)
- .await?;
+ .await
+ .wrap_api_err(
+ "executing `projects::clear_project_cache_and_queue_search`",
+ )?;
}
Ok(())
@@ -1224,7 +1235,8 @@ pub async fn update_issue_details(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("updating database records for `update_issue_details`")?;
let mut txn = pool
.begin()
@@ -1347,7 +1359,10 @@ pub async fn update_issue_details(
TechReviewExitReason::Resolved,
&mut txn,
)
- .await?;
+ .await
+ .wrap_api_err(
+ "executing `tech_review_sync::sync_project_tech_review_state`",
+ )?;
txn.commit()
.await
@@ -1381,7 +1396,10 @@ pub async fn update_global_issue_details(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err(
+ "updating database records for `update_global_issue_details`",
+ )?;
let updates = update_reqs.into_inner();
@@ -1461,7 +1479,10 @@ pub async fn update_global_issue_details(
TechReviewExitReason::Resolved,
&mut txn,
)
- .await?;
+ .await
+ .wrap_api_err(
+ "executing `tech_review_sync::sync_detail_key_tech_review_state`",
+ )?;
txn.commit()
.await
@@ -1498,7 +1519,8 @@ pub async fn add_report(
&session_queue,
Scopes::PROJECT_WRITE,
)
- .await?;
+ .await
+ .wrap_auth_err("inserting database records for `add_report`")?;
let file_id = add_report.file_id;
let mut txn = pool
diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
index c152d5c2f9..84fbe00fe3 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
@@ -148,7 +148,8 @@ pub async fn search_global_issue_details(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating global issue search")?;
let query = search_req
.query
@@ -370,7 +371,8 @@ pub async fn get_global_issue_detail(
&session_queue,
Scopes::PROJECT_READ,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating global issue detail request")?;
let detail_key = get_req.detail_key.trim();
if detail_key.is_empty() {
@@ -404,7 +406,7 @@ pub async fn get_global_issue_detail(
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to fetch global issue detail")?
- .ok_or(ApiError::NotFound)?;
+ .wrap_not_found_err("resource not found")?;
let local_rows = sqlx::query!(
r#"
diff --git a/apps/labrinth/src/routes/internal/mural.rs b/apps/labrinth/src/routes/internal/mural.rs
index b78069de5c..ea29c29e38 100644
--- a/apps/labrinth/src/routes/internal/mural.rs
+++ b/apps/labrinth/src/routes/internal/mural.rs
@@ -22,7 +22,7 @@ pub async fn get_bank_details(
let mural = payouts_queue.muralpay.load();
let mural = mural
.as_ref()
- .wrap_internal_err("Mural API not available")?;
+ .wrap_internal_err("required Mural API is not available")?;
let fiat_and_rail_codes = FiatAndRailCode::iter().collect::>();
let details = mural
.client
diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs
index 5c0d41b783..63e4f10eba 100644
--- a/apps/labrinth/src/routes/internal/pats.rs
+++ b/apps/labrinth/src/routes/internal/pats.rs
@@ -1,5 +1,6 @@
use crate::database;
use crate::database::models::generate_pat_id;
+use crate::util::error::Context as _;
use crate::auth::get_user_from_headers;
use crate::routes::ApiError;
@@ -18,7 +19,6 @@ use crate::database::models::notification_item::NotificationBuilder;
use crate::models::notifications::NotificationBody;
use crate::models::pats::{PersonalAccessToken, Scopes};
use crate::queue::session::AuthQueue;
-use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use validator::Validate;
@@ -54,7 +54,8 @@ pub async fn get_pats(
&session_queue,
Scopes::PAT_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let pat_ids =
@@ -63,11 +64,13 @@ pub async fn get_pats(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching personal access tokens from database")?;
let pats = database::models::pat_item::DBPersonalAccessToken::get_many_ids(
&pat_ids, &**pool, &redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching personal access tokens from database")?;
Ok(HttpResponse::Ok().json(
pats.into_iter()
@@ -104,19 +107,20 @@ pub async fn create_pat(
redis: Data,
session_queue: Data,
) -> Result {
- info.0.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ info.0
+ .validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
if info.scopes.is_restricted() {
- return Err(ApiError::InvalidInput(
- "Invalid scopes requested!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Invalid scopes requested!",
+ )));
}
if info.expires < Utc::now() {
- return Err(ApiError::InvalidInput(
- "Expire date must be in the future!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Expire date must be in the future!",
+ )));
}
let user = get_user_from_headers(
@@ -126,12 +130,18 @@ pub async fn create_pat(
&session_queue,
Scopes::PAT_CREATE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- let id = generate_pat_id(&mut transaction).await?;
+ let id = generate_pat_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating pat ID")?;
let token = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
@@ -152,7 +162,8 @@ pub async fn create_pat(
last_used: None,
}
.insert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `create_pat`")?;
NotificationBuilder {
body: NotificationBody::PatCreated {
@@ -160,14 +171,19 @@ pub async fn create_pat(
},
}
.insert(user.id.into(), &mut transaction, &redis)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_internal_err("inserting database records for `create_pat`")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::pat_item::DBPersonalAccessToken::clear_cache(
vec![(None, None, Some(user.id.into()))],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::Ok().json(PersonalAccessToken {
id: id.into(),
@@ -213,9 +229,10 @@ pub async fn edit_pat(
redis: Data,
session_queue: Data,
) -> Result {
- info.0.validate().map_err(|err| {
- ApiError::InvalidInput(validation_errors_to_string(err, None))
- })?;
+ info.0
+ .validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
let user = get_user_from_headers(
&req,
@@ -224,25 +241,30 @@ pub async fn edit_pat(
&session_queue,
Scopes::PAT_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::DBPersonalAccessToken::get(
&id, &**pool, &redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching personal access token from database")?;
if let Some(pat) = pat
&& pat.user_id == user.id.into()
{
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if let Some(scopes) = &info.scopes {
if scopes.is_restricted() {
- return Err(ApiError::InvalidInput(
- "Invalid scopes requested!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Invalid scopes requested!",
+ )));
}
sqlx::query!(
@@ -255,7 +277,8 @@ pub async fn edit_pat(
pat.id.0
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `edit_pat`")?;
}
if let Some(name) = &info.name {
sqlx::query!(
@@ -268,13 +291,14 @@ pub async fn edit_pat(
pat.id.0
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `edit_pat`")?;
}
if let Some(expires) = &info.expires {
if expires < &Utc::now() {
- return Err(ApiError::InvalidInput(
- "Expire date must be in the future!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Expire date must be in the future!",
+ )));
}
sqlx::query!(
@@ -287,15 +311,20 @@ pub async fn edit_pat(
pat.id.0
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `edit_pat`")?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::pat_item::DBPersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
}
Ok(HttpResponse::NoContent().finish())
@@ -330,29 +359,39 @@ pub async fn delete_pat(
&session_queue,
Scopes::PAT_DELETE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::DBPersonalAccessToken::get(
&id, &**pool, &redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching personal access token from database")?;
if let Some(pat) = pat
&& pat.user_id == user.id.into()
{
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
database::models::pat_item::DBPersonalAccessToken::remove(
pat.id,
&mut transaction,
)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_internal_err("deleting personal access token from database")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::pat_item::DBPersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
}
Ok(HttpResponse::NoContent().finish())
diff --git a/apps/labrinth/src/routes/internal/search.rs b/apps/labrinth/src/routes/internal/search.rs
index 6521a21800..04063ebfcc 100644
--- a/apps/labrinth/src/routes/internal/search.rs
+++ b/apps/labrinth/src/routes/internal/search.rs
@@ -1,3 +1,4 @@
+use crate::util::error::Context as _;
use crate::util::guards::admin_key_guard;
use crate::{
routes::ApiError,
@@ -19,7 +20,12 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
pub async fn tasks(
search: web::Data,
) -> Result, ApiError> {
- Ok(web::Json(search.tasks().await.map_err(ApiError::Internal)?))
+ Ok(web::Json(
+ search
+ .tasks()
+ .await
+ .wrap_internal_err("fetching search tasks")?,
+ ))
}
/// Cancel search tasks.
@@ -36,6 +42,6 @@ pub async fn tasks_cancel(
search
.tasks_cancel(&body)
.await
- .map_err(ApiError::Internal)?;
+ .wrap_internal_err("cancelling search tasks")?;
Ok(())
}
diff --git a/apps/labrinth/src/routes/internal/server_ping.rs b/apps/labrinth/src/routes/internal/server_ping.rs
index f80961bc88..5847572ea5 100644
--- a/apps/labrinth/src/routes/internal/server_ping.rs
+++ b/apps/labrinth/src/routes/internal/server_ping.rs
@@ -44,7 +44,8 @@ pub async fn ping_minecraft_java(
&session_queue,
Scopes::SESSION_ACCESS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let timeout = request.timeout_ms.map(Duration::from_millis);
server_ping::ping_server(&request.address, timeout)
diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs
index c6316a3bad..f7bd6f685c 100644
--- a/apps/labrinth/src/routes/internal/session.rs
+++ b/apps/labrinth/src/routes/internal/session.rs
@@ -9,6 +9,7 @@ use crate::models::pats::Scopes;
use crate::models::sessions::Session;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
use actix_web::http::header::AUTHORIZATION;
use actix_web::web::Data;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
@@ -159,20 +160,24 @@ pub async fn list(
&session_queue,
Scopes::SESSION_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let session = req
.headers()
.get(AUTHORIZATION)
.and_then(|x| x.to_str().ok())
- .ok_or_else(|| AuthenticationError::InvalidCredentials)?;
+ .ok_or_else(|| AuthenticationError::InvalidCredentials)
+ .wrap_auth_err("authenticating API request")?;
let session_ids =
DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching sessions from database")?;
let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis)
- .await?
+ .await
+ .wrap_internal_err("fetching sessions from database")?
.into_iter()
.filter(|x| x.expires > Utc::now())
.map(|x| Session::from(x, false, Some(session)))
@@ -211,17 +216,28 @@ pub async fn delete(
&session_queue,
Scopes::SESSION_DELETE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
+ let session = DBSession::get(info.into_inner().0, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching session from database")?;
if let Some(session) = session
&& session.user_id == current_user.id.into()
{
- let mut transaction = pool.begin().await?;
- DBSession::remove(session.id, &mut transaction).await?;
- transaction.commit().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
+ DBSession::remove(session.id, &mut transaction)
+ .await
+ .wrap_internal_err("deleting session from database")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
DBSession::clear_cache(
vec![(
Some(session.id),
@@ -230,7 +246,8 @@ pub async fn delete(
)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
}
Ok(HttpResponse::NoContent().body(""))
@@ -258,16 +275,14 @@ pub async fn refresh(
.headers()
.get(AUTHORIZATION)
.and_then(|x| x.to_str().ok())
- .ok_or_else(|| {
- ApiError::Authentication(AuthenticationError::InvalidCredentials)
- })?;
+ .wrap_auth_err_with(|| AuthenticationError::InvalidCredentials)?;
// We should ensure that the authorization given is a session token, and not some other type of token (like a PAT), since this endpoint is only for refreshing sessions.
// This is done by checking the prefix of the token, which should be "mra_" for session tokens.
if !session.starts_with("mra_") {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
let current_user = get_user_from_bearer_token(
@@ -278,23 +293,31 @@ pub async fn refresh(
&session_queue,
true, // Allow expired sessions, since we want to allow refreshing expired sessions
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- let session = DBSession::get(session, &**pool, &redis).await?;
+ let session = DBSession::get(session, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching session from database")?;
if let Some(session) = session {
if current_user.id != session.user_id.into()
|| session.refresh_expires < Utc::now()
{
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- DBSession::remove(session.id, &mut transaction).await?;
+ DBSession::remove(session.id, &mut transaction)
+ .await
+ .wrap_internal_err("deleting session from database")?;
let new_session = issue_session(
req,
session.user_id,
@@ -302,8 +325,12 @@ pub async fn refresh(
&redis,
Some(session.refresh_expires),
)
- .await?;
- transaction.commit().await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
DBSession::clear_cache(
vec![(
Some(session.id),
@@ -312,12 +339,13 @@ pub async fn refresh(
)],
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::Ok().json(Session::from(new_session, true, None)))
} else {
- Err(ApiError::Authentication(
+ Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ))
+ )))
}
}
diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs
index 160cdba9ba..62e4fde4d9 100644
--- a/apps/labrinth/src/routes/internal/statuses.rs
+++ b/apps/labrinth/src/routes/internal/statuses.rs
@@ -15,6 +15,7 @@ use crate::sync::friends::{FRIENDS_CHANNEL_NAME, RedisFriendsMessage};
use crate::sync::status::{
get_user_status, push_back_user_expiry, replace_user_status,
};
+use crate::util::error::Context as _;
use actix_web::web::{Data, Payload};
use actix_web::{HttpRequest, HttpResponse, get, web};
use actix_ws::Message;
@@ -67,15 +68,14 @@ pub async fn ws_init(
&session_queue,
false,
)
- .await?
- .ok_or_else(|| {
- ApiError::Authentication(AuthenticationError::InvalidCredentials)
- })?;
+ .await
+ .wrap_auth_err("authenticating API request")?
+ .wrap_auth_err_with(|| AuthenticationError::InvalidCredentials)?;
if !scopes.contains(Scopes::SESSION_ACCESS) {
- return Err(ApiError::Authentication(
+ return Err(ApiError::Auth(eyre::eyre!(
AuthenticationError::InvalidCredentials,
- ));
+ )));
}
let user = User::from_full(db_user);
@@ -93,7 +93,9 @@ pub async fn ws_init(
};
let friends =
- DBFriend::get_user_friends(user.id.into(), Some(true), &**pool).await?;
+ DBFriend::get_user_friends(user.id.into(), Some(true), &**pool)
+ .await
+ .wrap_internal_err("fetching friends from database")?;
let friend_statuses = if !friends.is_empty() {
let db = db.clone();
@@ -127,11 +129,12 @@ pub async fn ws_init(
};
let _ = session
- .text(serde_json::to_string(
- &ServerToClientMessage::FriendStatuses {
+ .text(
+ serde_json::to_string(&ServerToClientMessage::FriendStatuses {
statuses: friend_statuses,
- },
- )?)
+ })
+ .wrap_request_err("serializing friend statuses")?,
+ )
.await;
let unread_launcher_invites =
@@ -140,7 +143,8 @@ pub async fn ws_init(
&**pool,
&redis,
)
- .await?
+ .await
+ .wrap_internal_err("fetching notifications from database")?
.into_iter()
.filter(|notification| {
!notification.read
@@ -153,7 +157,12 @@ pub async fn ws_init(
.map(Notification::from);
for notification in unread_launcher_invites {
- let _ = session.text(serde_json::to_string(¬ification)?).await;
+ let _ = session
+ .text(
+ serde_json::to_string(¬ification)
+ .wrap_request_err("serializing launcher notification")?,
+ )
+ .await;
}
let db = db.clone();
@@ -168,12 +177,15 @@ pub async fn ws_init(
#[cfg(debug_assertions)]
tracing::info!("Connection {socket_id} opened by {}", user.id);
- replace_user_status(None, Some(&status), &redis).await?;
+ replace_user_status(None, Some(&status), &redis)
+ .await
+ .wrap_internal_err("reading HTTP response body")?;
broadcast_friends_message(
&redis,
RedisFriendsMessage::StatusUpdate { status },
)
- .await?;
+ .await
+ .wrap_internal_err("reading HTTP response body")?;
let (shutdown_sender, mut shutdown_receiver) =
tokio::sync::oneshot::channel::<()>();
diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs
index bc351ccb03..a10e7d724c 100644
--- a/apps/labrinth/src/routes/maven.rs
+++ b/apps/labrinth/src/routes/maven.rs
@@ -11,6 +11,8 @@ use crate::models::pats::Scopes;
use crate::models::projects::FileType;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context;
use crate::{auth::get_user_from_headers, database};
use actix_web::{HttpRequest, HttpResponse, get, route, web};
use quick_xml::escape::escape;
@@ -85,9 +87,11 @@ pub async fn maven_metadata(
) -> Result {
let project_id = params.into_inner().0;
let Some(project) =
- database::models::DBProject::get(&project_id, &**pool, &redis).await?
+ database::models::DBProject::get(&project_id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven project")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let user_option = get_user_from_headers(
@@ -101,8 +105,11 @@ pub async fn maven_metadata(
.map(|x| x.1)
.ok();
- if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_project(&project.inner, &user_option, &pool, false)
+ .await
+ .wrap_api_err("checking project visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
let version_names = sqlx::query!(
@@ -119,7 +126,8 @@ pub async fn maven_metadata(
.collect::>(),
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching version names from database")?;
let mut new_versions = Vec::new();
let mut vals = HashSet::new();
@@ -162,9 +170,11 @@ pub async fn maven_metadata(
},
};
- Ok(HttpResponse::Ok()
- .content_type("text/xml")
- .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?))
+ Ok(HttpResponse::Ok().content_type("text/xml").body(
+ yaserde::ser::to_string(&respdata)
+ .map_err(eyre::Report::msg)
+ .wrap_internal_err("serializing Maven metadata as XML")?,
+ ))
}
async fn find_version(
@@ -179,7 +189,8 @@ async fn find_version(
let all_versions =
database::models::DBVersion::get_many(&project.versions, pool, redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching versions from database")?;
let exact_matches = all_versions
.iter()
@@ -199,7 +210,8 @@ async fn find_version(
};
let db_loaders: HashSet = Loader::list(pool, redis)
- .await?
+ .await
+ .wrap_internal_err("fetching loader from Redis")?
.into_iter()
.map(|x| x.loader)
.collect();
@@ -310,9 +322,11 @@ pub async fn version_file(
) -> Result {
let (project_id, vnum, file) = params.into_inner();
let Some(project) =
- database::models::DBProject::get(&project_id, &**pool, &redis).await?
+ database::models::DBProject::get(&project_id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven project")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let user_option = get_user_from_headers(
@@ -326,17 +340,25 @@ pub async fn version_file(
.map(|x| x.1)
.ok();
- if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_project(&project.inner, &user_option, &pool, false)
+ .await
+ .wrap_api_err("checking project visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- let Some(version) = find_version(&project, &vnum, &pool, &redis).await?
+ let Some(version) = find_version(&project, &vnum, &pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven version")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
- if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_version(&version.inner, &user_option, &pool, &redis)
+ .await
+ .wrap_api_err("checking version visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
if file.eq_ignore_ascii_case(&format!("{}-{}.pom", &project_id, &vnum)) {
@@ -352,9 +374,11 @@ pub async fn version_file(
name: project.inner.name,
description: escape(project.inner.summary).into_owned(),
};
- return Ok(HttpResponse::Ok()
- .content_type("text/xml")
- .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
+ return Ok(HttpResponse::Ok().content_type("text/xml").body(
+ yaserde::ser::to_string(&respdata)
+ .map_err(eyre::Report::msg)
+ .wrap_internal_err("serializing Maven project as XML")?,
+ ));
} else if let Some(selected_file) =
find_file(&project_id, &vnum, &version, &file)
{
@@ -363,7 +387,7 @@ pub async fn version_file(
.body(""));
}
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
#[utoipa::path(
@@ -385,9 +409,11 @@ pub async fn version_file_sha1(
) -> Result {
let (project_id, vnum, file) = params.into_inner();
let Some(project) =
- database::models::DBProject::get(&project_id, &**pool, &redis).await?
+ database::models::DBProject::get(&project_id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven project")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let user_option = get_user_from_headers(
@@ -401,17 +427,25 @@ pub async fn version_file_sha1(
.map(|x| x.1)
.ok();
- if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_project(&project.inner, &user_option, &pool, false)
+ .await
+ .wrap_api_err("checking project visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- let Some(version) = find_version(&project, &vnum, &pool, &redis).await?
+ let Some(version) = find_version(&project, &vnum, &pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven version")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
- if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_version(&version.inner, &user_option, &pool, &redis)
+ .await
+ .wrap_api_err("checking version visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
Ok(find_file(&project_id, &vnum, &version, &file)
@@ -441,9 +475,11 @@ pub async fn version_file_sha512(
) -> Result {
let (project_id, vnum, file) = params.into_inner();
let Some(project) =
- database::models::DBProject::get(&project_id, &**pool, &redis).await?
+ database::models::DBProject::get(&project_id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven project")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
let user_option = get_user_from_headers(
@@ -457,17 +493,25 @@ pub async fn version_file_sha512(
.map(|x| x.1)
.ok();
- if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_project(&project.inner, &user_option, &pool, false)
+ .await
+ .wrap_api_err("checking project visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
- let Some(version) = find_version(&project, &vnum, &pool, &redis).await?
+ let Some(version) = find_version(&project, &vnum, &pool, &redis)
+ .await
+ .wrap_api_err("fetching Maven version")?
else {
- return Err(ApiError::NotFound);
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
- if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? {
- return Err(ApiError::NotFound);
+ if !is_visible_version(&version.inner, &user_option, &pool, &redis)
+ .await
+ .wrap_api_err("checking version visibility")?
+ {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
}
Ok(find_file(&project_id, &vnum, &version, &file)
diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs
index e9e606deba..fc5f72e75c 100644
--- a/apps/labrinth/src/routes/mod.rs
+++ b/apps/labrinth/src/routes/mod.rs
@@ -1,13 +1,10 @@
-use crate::database::models::DelphiReportIssueDetailsId;
use crate::env::ENV;
-use crate::file_hosting::FileHostingError;
use crate::util::cors::default_cors;
use actix_cors::Cors;
use actix_files::Files;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, web};
use futures::FutureExt;
-use serde_json::json;
use utoipa::openapi::extensions::ExtensionsBuilder;
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
@@ -192,84 +189,49 @@ pub enum ApiError {
/// Caller attempted a request which they are not allowed to make.
#[error(transparent)]
Auth(eyre::Report),
- #[error("Invalid input: {0}")]
- InvalidInput(String),
- #[error("Error while uploading file: {0}")]
- FileHosting(#[from] FileHostingError),
- #[error("database error")]
- Database(#[from] crate::database::models::DatabaseError),
- // todo: remove this variant
- #[error("Postgres database error: {0}")]
- SqlxDatabase(#[from] sqlx::Error),
- #[error("redis database error")]
- RedisDatabase(#[from] redis::RedisError),
- #[error("Clickhouse error: {0}")]
- Clickhouse(#[from] clickhouse::error::Error),
- #[error("XML error: {0}")]
- Xml(String),
- #[error("Deserialization error: {0}")]
- Json(#[from] serde_json::Error),
- #[error("Authentication error: {0}")]
- Authentication(#[from] crate::auth::AuthenticationError),
- #[error("Authentication error: {0}")]
- CustomAuthentication(String),
- #[error("Error while validating input: {0}")]
- Validation(String),
- #[error("Search error: {0}")]
- Search(#[from] meilisearch_sdk::errors::Error),
- #[error("Payments error: {0}")]
- Payments(String),
- #[error("Discord error: {0}")]
- Discord(String),
- #[error("Slack webhook error: {0}")]
- Slack(String),
- #[error("Captcha error. Try resubmitting the form.")]
- Turnstile,
- #[error("Error while decoding Base62: {0}")]
- Decoding(#[from] ariadne::ids::DecodingError),
- #[error("Image parsing error: {0}")]
- ImageParse(#[from] image::ImageError),
- #[error("Password hashing error: {0}")]
- PasswordHashing(#[from] argon2::password_hash::Error),
- #[error("{0}")]
- Mail(#[from] crate::queue::email::MailError),
- #[error("Error while rerouting request: {0:?}")]
- Reroute(#[from] reqwest::Error),
- #[error("Unable to read zip archive: {0}")]
- Zip(#[from] zip::result::ZipError),
- #[error("IO Error: {0}")]
- Io(#[from] std::io::Error),
- #[error("Resource not found")]
- NotFound,
- #[error("Conflict: {0}")]
- Conflict(String),
- #[error("precondition required: {0}")]
- PreconditionRequired(String),
- #[error("precondition failed: {0}")]
- PreconditionFailed(String),
- #[error("External tax compliance API error")]
- TaxComplianceApi,
+ /// The requested resource does not exist.
#[error(transparent)]
- TaxProcessor(#[from] crate::util::anrok::AnrokError),
- #[error(
- "You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
- )]
- RateLimitError(u128, u32),
- #[error("Error while interacting with payment processor: {0}")]
- Stripe(#[from] stripe::StripeError),
- #[error("Error while interacting with Delphi: {0:?}")]
- Delphi(eyre::Error),
+ NotFound(eyre::Report),
+ /// The request conflicts with the current state of the resource.
#[error(transparent)]
- Mural(#[from] Box),
- #[error("report still has {} issue details with no verdict", details.len())]
- TechReviewDetailsWithNoVerdict {
- details: Vec,
- },
+ Conflict(eyre::Report),
+ /// A service dependency failed to complete the request.
+ #[error(transparent)]
+ FailedDependency(eyre::Report),
+ /// The request requires a precondition.
+ #[error(transparent)]
+ PreconditionRequired(eyre::Report),
+ /// A request precondition was not met.
+ #[error(transparent)]
+ PreconditionFailed(eyre::Report),
+ /// The caller exceeded a request rate limit.
+ #[error(transparent)]
+ RateLimit(eyre::Report),
}
impl ApiError {
- pub fn delphi(err: impl Into) -> Self {
- Self::Delphi(err.into())
+ /// Adds context to the contained report while preserving the HTTP status.
+ pub(crate) fn wrap_err(self, msg: D) -> Self
+ where
+ D: Send + Sync + std::fmt::Debug + std::fmt::Display + 'static,
+ {
+ match self {
+ Self::Internal(report) => Self::Internal(report.wrap_err(msg)),
+ Self::Request(report) => Self::Request(report.wrap_err(msg)),
+ Self::Auth(report) => Self::Auth(report.wrap_err(msg)),
+ Self::NotFound(report) => Self::NotFound(report.wrap_err(msg)),
+ Self::Conflict(report) => Self::Conflict(report.wrap_err(msg)),
+ Self::FailedDependency(report) => {
+ Self::FailedDependency(report.wrap_err(msg))
+ }
+ Self::PreconditionRequired(report) => {
+ Self::PreconditionRequired(report.wrap_err(msg))
+ }
+ Self::PreconditionFailed(report) => {
+ Self::PreconditionFailed(report.wrap_err(msg))
+ }
+ Self::RateLimit(report) => Self::RateLimit(report.wrap_err(msg)),
+ }
}
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
@@ -278,60 +240,15 @@ impl ApiError {
Self::Internal(..) => "internal_error",
Self::Request(..) => "request_error",
Self::Auth(..) => "auth_error",
- Self::Database(..) => "database_error",
- Self::SqlxDatabase(..) => "database_error",
- Self::RedisDatabase(..) => "database_error",
- Self::Authentication(..) => "unauthorized",
- Self::CustomAuthentication(..) => "unauthorized",
- Self::Xml(..) => "xml_error",
- Self::Json(..) => "json_error",
- Self::Search(..) => "search_error",
- Self::FileHosting(..) => "file_hosting_error",
- Self::InvalidInput(..) => "invalid_input",
- Self::Validation(..) => "invalid_input",
- Self::Payments(..) => "payments_error",
- Self::Discord(..) => "discord_error",
- Self::Turnstile => "turnstile_error",
- Self::Decoding(..) => "decoding_error",
- Self::ImageParse(..) => "invalid_image",
- Self::PasswordHashing(..) => "password_hashing_error",
- Self::Mail(..) => "mail_error",
- Self::Clickhouse(..) => "clickhouse_error",
- Self::Reroute(..) => "reroute_error",
- Self::NotFound => "not_found",
+ Self::NotFound(..) => "not_found",
Self::Conflict(..) => "conflict",
+ Self::FailedDependency(..) => "failed_dependency",
Self::PreconditionRequired(..) => "precondition_required",
Self::PreconditionFailed(..) => "precondition_failed",
- Self::TaxComplianceApi => "tax_compliance_api_error",
- Self::Zip(..) => "zip_error",
- Self::Io(..) => "io_error",
- Self::RateLimitError(..) => "ratelimit_error",
- Self::Stripe(..) => "stripe_error",
- Self::TaxProcessor(..) => "tax_processor_error",
- Self::Slack(..) => "slack_error",
- Self::Delphi(..) => "delphi_error",
- Self::Mural(..) => "mural_error",
- Self::TechReviewDetailsWithNoVerdict { .. } => {
- "tech_review_issues_with_no_verdict"
- }
- },
- description: match self {
- Self::Internal(e) => format!("{e:#}"),
- Self::Request(e) => format!("{e:#}"),
- Self::Auth(e) => format!("{e:#}"),
- _ => self.to_string(),
- },
- details: match self {
- Self::Mural(err) => serde_json::to_value(err.clone()).ok(),
- Self::TechReviewDetailsWithNoVerdict { details } => {
- let details = serde_json::to_value(details)
- .expect("details should never fail to serialize");
- Some(json!({
- "issue_details": details
- }))
- }
- _ => None,
+ Self::RateLimit(..) => "ratelimit_error",
},
+ description: format!("{self:#}"),
+ details: None,
}
}
}
@@ -342,42 +259,12 @@ impl actix_web::ResponseError for ApiError {
Self::Internal(..) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Request(..) => StatusCode::BAD_REQUEST,
Self::Auth(..) => StatusCode::UNAUTHORIZED,
- Self::InvalidInput(..) => StatusCode::BAD_REQUEST,
- Self::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::RedisDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Clickhouse(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Authentication(..) => StatusCode::UNAUTHORIZED,
- Self::CustomAuthentication(..) => StatusCode::UNAUTHORIZED,
- Self::Xml(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Json(..) => StatusCode::BAD_REQUEST,
- Self::Search(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Validation(..) => StatusCode::BAD_REQUEST,
- Self::Payments(..) => StatusCode::FAILED_DEPENDENCY,
- Self::Discord(..) => StatusCode::FAILED_DEPENDENCY,
- Self::Turnstile => StatusCode::BAD_REQUEST,
- Self::Decoding(..) => StatusCode::BAD_REQUEST,
- Self::ImageParse(..) => StatusCode::BAD_REQUEST,
- Self::PasswordHashing(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::NotFound => StatusCode::NOT_FOUND,
+ Self::NotFound(..) => StatusCode::NOT_FOUND,
Self::Conflict(..) => StatusCode::CONFLICT,
+ Self::FailedDependency(..) => StatusCode::FAILED_DEPENDENCY,
Self::PreconditionRequired(..) => StatusCode::PRECONDITION_REQUIRED,
Self::PreconditionFailed(..) => StatusCode::PRECONDITION_FAILED,
- Self::TaxComplianceApi => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Zip(..) => StatusCode::BAD_REQUEST,
- Self::Io(..) => StatusCode::BAD_REQUEST,
- Self::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
- Self::Stripe(..) => StatusCode::FAILED_DEPENDENCY,
- Self::TaxProcessor(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Delphi(..) => StatusCode::INTERNAL_SERVER_ERROR,
- Self::Mural(..) => StatusCode::BAD_REQUEST,
- Self::TechReviewDetailsWithNoVerdict { .. } => {
- StatusCode::BAD_REQUEST
- }
+ Self::RateLimit(..) => StatusCode::TOO_MANY_REQUESTS,
}
}
diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs
index 7b287c6cf9..95db80199d 100644
--- a/apps/labrinth/src/routes/updates.rs
+++ b/apps/labrinth/src/routes/updates.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::cmp::Reverse;
use std::collections::HashMap;
@@ -55,8 +57,9 @@ pub async fn forge_updates(
let (id,) = info.into_inner();
let project = database::models::DBProject::get(&id, &**pool, &redis)
- .await?
- .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
+ .await
+ .wrap_api_err("fetching project from database")?
+ .wrap_request_err_with(|| ERROR.to_string())?;
let user_option = get_user_from_headers(
&req,
@@ -69,8 +72,11 @@ pub async fn forge_updates(
.map(|x| x.1)
.ok();
- if !is_visible_project(&project.inner, &user_option, &pool, false).await? {
- return Err(ApiError::InvalidInput(ERROR.to_string()));
+ if !is_visible_project(&project.inner, &user_option, &pool, false)
+ .await
+ .wrap_api_err("checking project visibility")?
+ {
+ return Err(ApiError::Request(eyre::eyre!("{ERROR}")));
}
let versions = database::models::DBVersion::get_many(
@@ -78,7 +84,8 @@ pub async fn forge_updates(
&***ro_pool,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching versions from database")?;
let loaders = match &*neo.neoforge {
"only" => |x: &String| *x == "neoforge",
@@ -96,7 +103,8 @@ pub async fn forge_updates(
&ro_pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching compatible update versions")?;
versions.sort_by_key(|b| Reverse(b.date_published));
diff --git a/apps/labrinth/src/routes/v2/notifications.rs b/apps/labrinth/src/routes/v2/notifications.rs
index f84f58ab5d..52f279fa6c 100644
--- a/apps/labrinth/src/routes/v2/notifications.rs
+++ b/apps/labrinth/src/routes/v2/notifications.rs
@@ -6,6 +6,7 @@ use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::v2_reroute;
use crate::routes::v3;
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize};
use xredis::RedisPool;
@@ -65,7 +66,11 @@ pub async fn notifications_get(
)
.await
.or_else(v2_reroute::flatten_404_error);
- match v2_reroute::extract_ok_json::>(resp?).await {
+ match v2_reroute::extract_ok_json::>(
+ resp.wrap_api_err("extracting v2 response body")?,
+ )
+ .await
+ {
Ok(notifications) => {
let notifications: Vec = notifications
.into_iter()
@@ -115,7 +120,8 @@ pub async fn notification_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
match v2_reroute::extract_ok_json::(response).await {
Ok(notification) => {
let notification = LegacyNotification::from(notification);
diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs
index d36bae318f..407d6359b2 100644
--- a/apps/labrinth/src/routes/v2/projects.rs
+++ b/apps/labrinth/src/routes/v2/projects.rs
@@ -13,6 +13,8 @@ use crate::queue::session::AuthQueue;
use crate::routes::v3::projects::ProjectIds;
use crate::routes::{ApiError, v2_reroute, v3};
use crate::search::{SearchBackend, SearchRequest, SearchState};
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -79,7 +81,8 @@ pub async fn project_search(
// While the backend for this has changed, it doesnt affect much
// in the API calls except that 'versions:x' is now 'game_versions:x'
let facets: Option>> = if let Some(facets) = info.facets {
- let facets = serde_json::from_str::>>(&facets)?;
+ let facets = serde_json::from_str::>>(&facets)
+ .wrap_request_err("deserializing JSON data")?;
Some(
facets
@@ -119,7 +122,10 @@ pub async fn project_search(
..info
};
- let results = search_backend.search_for_project(&info, &redis).await?;
+ let results = search_backend
+ .search_for_project(&info, &redis)
+ .await
+ .wrap_api_err("searching projects")?;
let results = LegacySearchResults::from(results);
@@ -194,12 +200,17 @@ pub async fn random_projects_get(
)
.await
.or_else(v2_reroute::flatten_404_error)
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
Ok(project) => {
let legacy_projects =
- LegacyProject::from_many(project, &**pool, &redis).await?;
+ LegacyProject::from_many(project, &**pool, &redis)
+ .await
+ .wrap_internal_err(
+ "executing `LegacyProject::from_many`",
+ )?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
@@ -234,13 +245,18 @@ pub async fn projects_get(
)
.await
.or_else(v2_reroute::flatten_404_error)
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
Ok(project) => {
let legacy_projects =
- LegacyProject::from_many(project, &**pool, &redis).await?;
+ LegacyProject::from_many(project, &**pool, &redis)
+ .await
+ .wrap_internal_err(
+ "executing `LegacyProject::from_many`",
+ )?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
@@ -284,14 +300,18 @@ pub async fn project_get(
.await
{
Ok(resp) => resp.0,
- Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
+ Err(ApiError::NotFound(_)) => {
+ return Ok(HttpResponse::NotFound().body(""));
+ }
Err(err) => return Err(err),
};
// Convert response to V2 format
let version_item = match project.versions.first() {
Some(vid) => {
- version_item::DBVersion::get((*vid).into(), &**pool, &redis).await?
+ version_item::DBVersion::get((*vid).into(), &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching version from database")?
}
None => None,
};
@@ -371,7 +391,8 @@ pub async fn dependency_list(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
match v2_reroute::extract_ok_json::<
crate::routes::v3::projects::DependencyInfo,
@@ -384,7 +405,10 @@ pub async fn dependency_list(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "converting dependency projects to legacy responses",
+ )?;
let converted_versions = dependency_info
.versions
.into_iter()
@@ -585,7 +609,9 @@ pub async fn project_edit(
if let Some(donation_urls) = v2_new_project.donation_urls {
// Fetch current donation links from project so we know what to delete
let fetched_example_project =
- project_item::DBProject::get(&info.0, &**pool, &redis).await?;
+ project_item::DBProject::get(&info.0, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching project from database")?;
let donation_links = fetched_example_project
.map(|x| {
x.urls
@@ -647,7 +673,8 @@ pub async fn project_edit(
search_state.clone(),
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// If client and server side were set, we will call
// the version setting route for each version to set the side types for each of them.
@@ -659,11 +686,13 @@ pub async fn project_edit(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching project from database")?;
let version_ids = project_item.map(|x| x.versions).unwrap_or_default();
let versions =
version_item::DBVersion::get_many(&version_ids, &**pool, &redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching versions from database")?;
for version in versions {
let version = Version::from(version);
let mut fields = version.fields;
@@ -690,7 +719,8 @@ pub async fn project_edit(
session_queue.clone(),
search_state.clone(),
)
- .await?;
+ .await
+ .wrap_api_err("editing project")?;
}
}
Ok(response)
@@ -795,7 +825,9 @@ pub async fn projects_edit(
// If we are *setting* donation links, we will set every possible donation link to None, as
// setting will delete all of them then 're-add' the ones we want to keep
if let Some(donation_url) = bulk_edit_project.donation_urls {
- let link_platforms = LinkPlatform::list(&**pool, &redis).await?;
+ let link_platforms = LinkPlatform::list(&**pool, &redis)
+ .await
+ .wrap_internal_err("fetching link platform from Redis")?;
for link in link_platforms {
if link.donation {
link_urls.insert(link.name, None);
diff --git a/apps/labrinth/src/routes/v2/reports.rs b/apps/labrinth/src/routes/v2/reports.rs
index c50950e544..041900e5f7 100644
--- a/apps/labrinth/src/routes/v2/reports.rs
+++ b/apps/labrinth/src/routes/v2/reports.rs
@@ -3,6 +3,7 @@ use crate::models::reports::Report;
use crate::models::v2::reports::LegacyReport;
use crate::queue::session::AuthQueue;
use crate::routes::{ApiError, v2_reroute, v3};
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use serde::Deserialize;
use validator::Validate;
@@ -44,7 +45,8 @@ pub async fn report_create(
let response =
v3::reports::report_create(req, pool, body, redis, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
@@ -109,7 +111,8 @@ pub async fn reports(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
@@ -164,7 +167,8 @@ pub async fn reports_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
@@ -209,7 +213,8 @@ pub async fn report_get(
let response =
v3::reports::report_get(req, pool, redis, info, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
diff --git a/apps/labrinth/src/routes/v2/statistics.rs b/apps/labrinth/src/routes/v2/statistics.rs
index 94ac6f66e9..fed3166c32 100644
--- a/apps/labrinth/src/routes/v2/statistics.rs
+++ b/apps/labrinth/src/routes/v2/statistics.rs
@@ -3,6 +3,7 @@ use crate::routes::{
ApiError, v2_reroute,
v3::{self, statistics::V3Stats},
};
+use crate::util::error::ApiContext as _;
use actix_web::{HttpResponse, get, web};
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
@@ -36,7 +37,8 @@ pub async fn get_stats(
) -> Result {
let response = v3::statistics::get_stats(pool)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
match v2_reroute::extract_ok_json::(response).await {
Ok(stats) => {
diff --git a/apps/labrinth/src/routes/v2/tags.rs b/apps/labrinth/src/routes/v2/tags.rs
index 56a86ca46d..ac14c0b412 100644
--- a/apps/labrinth/src/routes/v2/tags.rs
+++ b/apps/labrinth/src/routes/v2/tags.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use std::collections::HashMap;
use super::ApiError;
@@ -54,7 +55,9 @@ pub async fn category_list(
pool: web::Data,
redis: web::Data,
) -> Result {
- let response = v3::tags::category_list(pool, redis).await?;
+ let response = v3::tags::category_list(pool, redis)
+ .await
+ .wrap_api_err("executing `tags::category_list`")?;
// Convert to V2 format
match v2_reroute::extract_ok_json::>(response)
@@ -102,7 +105,9 @@ pub async fn loader_list(
pool: web::Data,
redis: web::Data,
) -> Result {
- let response = v3::tags::loader_list(pool, redis).await?;
+ let response = v3::tags::loader_list(pool, redis)
+ .await
+ .wrap_api_err("executing `tags::loader_list`")?;
// Convert to V2 format
match v2_reroute::extract_ok_json::>(response)
@@ -198,7 +203,8 @@ pub async fn game_version_list(
}),
redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching game versions")?;
// Convert to V2 format
Ok(
@@ -293,7 +299,8 @@ pub async fn license_text(
) -> Result {
let license = v3::tags::license_text(params)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert to V2 format
Ok(
@@ -339,7 +346,9 @@ pub async fn donation_platform_list(
pool: web::Data,
redis: web::Data,
) -> Result {
- let response = v3::tags::link_platform_list(pool, redis).await?;
+ let response = v3::tags::link_platform_list(pool, redis)
+ .await
+ .wrap_api_err("executing `tags::link_platform_list`")?;
// Convert to V2 format
Ok(
diff --git a/apps/labrinth/src/routes/v2/teams.rs b/apps/labrinth/src/routes/v2/teams.rs
index 36c373094e..cdd4e30665 100644
--- a/apps/labrinth/src/routes/v2/teams.rs
+++ b/apps/labrinth/src/routes/v2/teams.rs
@@ -6,6 +6,7 @@ use crate::models::teams::{
use crate::models::v2::teams::LegacyTeamMember;
use crate::queue::session::AuthQueue;
use crate::routes::{ApiError, v2_reroute, v3};
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
use ariadne::ids::UserId;
use rust_decimal::Decimal;
@@ -63,7 +64,8 @@ pub async fn team_members_get_project(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
Ok(members) => {
@@ -101,7 +103,8 @@ pub async fn team_members_get(
let response =
v3::teams::team_members_get(req, info, pool, redis, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
Ok(members) => {
@@ -148,7 +151,11 @@ pub async fn teams_get(
.await
.or_else(v2_reroute::flatten_404_error);
// Convert response to V2 format
- match v2_reroute::extract_ok_json::>>(response?).await {
+ match v2_reroute::extract_ok_json::>>(
+ response.wrap_api_err("extracting v2 response body")?,
+ )
+ .await
+ {
Ok(members) => {
let members = members
.into_iter()
diff --git a/apps/labrinth/src/routes/v2/threads.rs b/apps/labrinth/src/routes/v2/threads.rs
index 3854d4977a..e110ef5b98 100644
--- a/apps/labrinth/src/routes/v2/threads.rs
+++ b/apps/labrinth/src/routes/v2/threads.rs
@@ -5,6 +5,7 @@ use crate::models::threads::{MessageBody, Thread};
use crate::models::v2::threads::LegacyThread;
use crate::queue::session::AuthQueue;
use crate::routes::{ApiError, v2_reroute, v3};
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use serde::Deserialize;
use xredis::RedisPool;
@@ -88,7 +89,8 @@ pub async fn threads_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
diff --git a/apps/labrinth/src/routes/v2/users.rs b/apps/labrinth/src/routes/v2/users.rs
index 5879315336..3c5dae8e8e 100644
--- a/apps/labrinth/src/routes/v2/users.rs
+++ b/apps/labrinth/src/routes/v2/users.rs
@@ -8,6 +8,8 @@ use crate::models::v2::projects::LegacyProject;
use crate::models::v2::user::LegacyUser;
use crate::queue::session::AuthQueue;
use crate::routes::{ApiError, v2_reroute, v3};
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -53,7 +55,8 @@ pub async fn user_auth_get(
) -> Result {
let response = v3::users::user_auth_get(req, pool, redis, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
@@ -96,7 +99,8 @@ pub async fn users_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
@@ -136,7 +140,8 @@ pub async fn user_get(
) -> Result {
let response = v3::users::user_get(req, info, pool, redis, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
@@ -181,13 +186,18 @@ pub async fn projects_list(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert to V2 projects
match v2_reroute::extract_ok_json::>(response).await {
Ok(project) => {
let legacy_projects =
- LegacyProject::from_many(project, &**pool, &redis).await?;
+ LegacyProject::from_many(project, &**pool, &redis)
+ .await
+ .wrap_internal_err(
+ "executing `LegacyProject::from_many`",
+ )?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
@@ -448,13 +458,18 @@ pub async fn user_follows(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert to V2 projects
match v2_reroute::extract_ok_json::>(response).await {
Ok(project) => {
let legacy_projects =
- LegacyProject::from_many(project, &**pool, &redis).await?;
+ LegacyProject::from_many(project, &**pool, &redis)
+ .await
+ .wrap_internal_err(
+ "executing `LegacyProject::from_many`",
+ )?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
@@ -494,7 +509,8 @@ pub async fn user_notifications(
let response =
v3::users::user_notifications(req, info, pool, redis, session_queue)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
Ok(notifications) => {
diff --git a/apps/labrinth/src/routes/v2/version_file.rs b/apps/labrinth/src/routes/v2/version_file.rs
index a4c529b8c1..f1db94c0bf 100644
--- a/apps/labrinth/src/routes/v2/version_file.rs
+++ b/apps/labrinth/src/routes/v2/version_file.rs
@@ -7,6 +7,8 @@ use crate::queue::session::AuthQueue;
use crate::routes::HashAlgorithm;
use crate::routes::v3::version_file::{DownloadRedirect, HashQuery};
use crate::routes::{FileHash, v2_reroute, v3};
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -69,7 +71,8 @@ pub async fn get_version_from_hash(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
@@ -233,7 +236,8 @@ pub async fn get_update_from_hash(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
@@ -289,7 +293,8 @@ pub async fn get_versions_from_hashes(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert to V2
match v2_reroute::extract_ok_json::>(response)
@@ -342,7 +347,8 @@ pub async fn get_projects_from_hashes(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert to V2
match v2_reroute::extract_ok_json::>(response)
@@ -361,7 +367,8 @@ pub async fn get_projects_from_hashes(
&**pool,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("converting projects to legacy responses")?;
let legacy_projects_hashes = hash_to_project_id
.into_iter()
.filter_map(|(hash, project_id)| {
@@ -426,7 +433,9 @@ pub async fn update_files(
.await
{
Ok(resp) => resp,
- Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
+ Err(ApiError::NotFound(_)) => {
+ return Ok(HttpResponse::NotFound().body(""));
+ }
Err(err) => return Err(err),
};
@@ -477,7 +486,9 @@ pub async fn update_files_many(
.await
{
Ok(resp) => resp,
- Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
+ Err(ApiError::NotFound(_)) => {
+ return Ok(HttpResponse::NotFound().body(""));
+ }
Err(err) => return Err(err),
};
@@ -565,7 +576,8 @@ pub async fn update_individual_files(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response)
diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs
index 2ffb4abd93..ec94f169ba 100644
--- a/apps/labrinth/src/routes/v2/versions.rs
+++ b/apps/labrinth/src/routes/v2/versions.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::collections::HashMap;
use super::ApiError;
@@ -138,7 +140,8 @@ pub async fn version_list(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
@@ -191,7 +194,8 @@ pub async fn version_project_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
Ok(version) => {
@@ -242,7 +246,8 @@ pub async fn versions_get(
session_queue,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::>(response).await {
@@ -294,7 +299,8 @@ pub async fn version_get(
)
.await
.map(|b| HttpResponse::Ok().json(b))
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("flattening v2 not-found response")?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::(response).await {
Ok(version) => {
@@ -396,7 +402,9 @@ pub async fn version_edit(
.await
{
Ok(resp) => resp,
- Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
+ Err(ApiError::NotFound(_)) => {
+ return Ok(HttpResponse::NotFound().body(""));
+ }
Err(err) => return Err(err),
};
let old_version = match v2_reroute::extract_ok_json::(
@@ -450,12 +458,16 @@ pub async fn version_edit(
info,
pool,
redis,
- web::Json(serde_json::to_value(new_version)?),
+ web::Json(
+ serde_json::to_value(new_version)
+ .wrap_request_err("serializing version edit")?,
+ ),
session_queue,
search_state,
)
.await
- .or_else(v2_reroute::flatten_404_error)?;
+ .or_else(v2_reroute::flatten_404_error)
+ .wrap_api_err("editing version through v3 route")?;
Ok(response)
}
diff --git a/apps/labrinth/src/routes/v2_reroute.rs b/apps/labrinth/src/routes/v2_reroute.rs
index 0a99b0796c..5576c0f2fa 100644
--- a/apps/labrinth/src/routes/v2_reroute.rs
+++ b/apps/labrinth/src/routes/v2_reroute.rs
@@ -45,7 +45,7 @@ where
// This should not be used on the fallback no-route-found handler
pub fn flatten_404_error(res: ApiError) -> Result {
match res {
- ApiError::NotFound => Ok(HttpResponse::NotFound().body("")),
+ ApiError::NotFound(_) => Ok(HttpResponse::NotFound().body("")),
_ => Err(res),
}
}
diff --git a/apps/labrinth/src/routes/v3/analytics_get/facets/fixed.rs b/apps/labrinth/src/routes/v3/analytics_get/facets/fixed.rs
index 14b1f68911..02a37254bb 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/facets/fixed.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/facets/fixed.rs
@@ -3,6 +3,7 @@ use super::{
AnalyticsFacets, ProjectDownloadsFacets, ProjectPlaytimeFacets,
ProjectViewsFacets,
};
+use crate::util::error::ApiContext as _;
use crate::{
database::PgPool, models::v3::analytics::DownloadReason, routes::ApiError,
util::tags::valid_download_tags,
@@ -13,7 +14,9 @@ pub async fn fetch(
pool: &PgPool,
redis: &RedisPool,
) -> Result {
- let tags = valid_download_tags(pool, redis).await?;
+ let tags = valid_download_tags(pool, redis)
+ .await
+ .wrap_api_err("executing `valid_download_tags`")?;
let mut loaders = tags.loaders.iter().cloned().collect::>();
loaders.sort();
let mut game_versions =
diff --git a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs
index 02218bcec9..c61ea4f565 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/facets/mod.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use xredis::RedisPool;
mod fixed;
@@ -78,9 +80,12 @@ pub async fn fetch_facets(
&session_queue,
Scopes::ANALYTICS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let facets = fixed::fetch(&pool, &redis).await?;
+ let facets = fixed::fetch(&pool, &redis)
+ .await
+ .wrap_api_err("executing `fixed::fetch`")?;
Ok(web::Json(FacetsResponse { facets }))
}
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_conversions.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_conversions.rs
index c62f1e0e0b..29e611ccab 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_conversions.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_conversions.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
@@ -91,7 +92,12 @@ pub(crate) async fn fetch(
&filter_affiliate_code_ids,
)
.fetch(pool);
- while let Some(row) = rows.next().await.transpose()? {
+ while let Some(row) = rows
+ .next()
+ .await
+ .transpose()
+ .wrap_internal_err("fetching affiliate code conversions")?
+ {
let bucket = row
.bucket
.wrap_internal_err("bucket should be non-null - query bug!")?;
@@ -116,7 +122,8 @@ pub(crate) async fn fetch(
AffiliateCodeConversions { conversions },
),
}),
- )?;
+ )
+ .wrap_api_err("executing `AffiliateCodeMetrics::Conversions`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_revenue.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_revenue.rs
index 9873bc15cd..791dd8a3dd 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_revenue.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/affiliate_code_revenue.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use futures::StreamExt;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
@@ -86,7 +87,12 @@ pub(crate) async fn fetch(
&filter_affiliate_code_ids,
)
.fetch(pool);
- while let Some(row) = rows.next().await.transpose()? {
+ while let Some(row) = rows
+ .next()
+ .await
+ .transpose()
+ .wrap_internal_err("fetching affiliate code revenue")?
+ {
let bucket = row
.bucket
.wrap_internal_err("bucket should be non-null - query bug!")?;
@@ -110,7 +116,8 @@ pub(crate) async fn fetch(
revenue,
}),
}),
- )?;
+ )
+ .wrap_api_err("executing `AffiliateCodeMetrics::Revenue`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs
index 36e8aabe16..2439d9cb5c 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use std::{
collections::{HashMap, HashSet},
sync::{
@@ -14,7 +15,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use crate::{
database::{
PgPool,
- models::{DBProjectId, DBVersion, DBVersionId},
+ models::{DBProjectId, DBVersionId},
},
models::{
ids::{ProjectId, VersionId},
@@ -326,37 +327,28 @@ async fn fetch_dependent_version_projects(
return Ok(HashMap::new());
}
- let dependent_on_version_ids =
- dependent_on_version_ids.into_iter().collect::>();
- let versions =
- DBVersion::get_many(&dependent_on_version_ids, cx.pool, cx.redis)
- .await?;
-
- let dependent_project_ids = versions
- .iter()
- .map(|version| version.inner.project_id.0)
+ let dependent_on_version_ids = dependent_on_version_ids
+ .into_iter()
+ .map(|version_id| version_id.0)
.collect::>();
- let server_projects = sqlx::query!(
+ let versions = sqlx::query!(
"
- SELECT id FROM mods
- WHERE id = ANY($1)
- AND components ? 'minecraft_server'
+ SELECT v.id, v.mod_id
+ FROM versions v
+ INNER JOIN mods m ON m.id = v.mod_id
+ WHERE
+ v.id = ANY($1)
+ AND jsonb_typeof(m.components -> 'minecraft_server') IS DISTINCT FROM 'object'
",
- &dependent_project_ids,
+ &dependent_on_version_ids,
)
.fetch_all(cx.pool)
.await
- .wrap_internal_err("failed to fetch server dependent projects")?
- .into_iter()
- .map(|project| DBProjectId(project.id))
- .collect::>();
+ .wrap_internal_err("failed to fetch dependent version projects")?;
Ok(versions
.into_iter()
- .filter_map(|version| {
- (!server_projects.contains(&version.inner.project_id))
- .then_some((version.inner.id, version.inner.project_id))
- })
+ .map(|version| (DBVersionId(version.id), DBProjectId(version.mod_id)))
.collect())
}
@@ -367,7 +359,9 @@ pub(crate) async fn fetch(
use ProjectDownloadsField as F;
let uses = |field| metrics.bucket_by.contains(&field);
let dependent_on_version_filter =
- fetch_dependent_on_version_filter(metrics, cx.pool).await?;
+ fetch_dependent_on_version_filter(metrics, cx.pool)
+ .await
+ .wrap_api_err("fetching dependent on version filter")?;
if !metrics.filter_by.dependent_project_id.is_empty()
&& dependent_on_version_filter.is_empty()
{
@@ -442,15 +436,23 @@ pub(crate) async fn fetch(
.iter()
.any(|(column_name, used)| *column_name == name && *used)
};
- let mut cursor = query.fetch::()?;
+ let mut cursor = query
+ .fetch::()
+ .wrap_internal_err("fetching project-download pagination cursor")?;
let mut rows = Vec::new();
- while let Some(row) = cursor.next().await? {
+ while let Some(row) = cursor
+ .next()
+ .await
+ .wrap_internal_err("fetching project downloads")?
+ {
rows.push(row);
}
let dependent_version_projects =
- fetch_dependent_version_projects(&rows, cx).await?;
+ fetch_dependent_version_projects(&rows, cx)
+ .await
+ .wrap_api_err("fetching dependent version projects")?;
let mut buckets = HashMap::::new();
for row in rows {
@@ -576,7 +578,8 @@ pub(crate) async fn fetch(
downloads,
}),
}),
- )?;
+ )
+ .wrap_api_err("executing `ProjectMetrics::Downloads`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_playtime.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_playtime.rs
index 91ce1cfc10..f5ce8767b0 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_playtime.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_playtime.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::collections::HashMap;
use const_format::formatcp;
@@ -227,10 +229,16 @@ pub(crate) async fn fetch(
query = filter_param.bind(query);
}
- let mut cursor = query.fetch::()?;
+ let mut cursor = query
+ .fetch::()
+ .wrap_internal_err("fetching project-playtime pagination cursor")?;
let mut buckets = HashMap::::new();
- while let Some(row) = cursor.next().await? {
+ while let Some(row) = cursor
+ .next()
+ .await
+ .wrap_internal_err("fetching project playtime")?
+ {
let project_id =
if uses_column("use_project_id") && row.project_id.0 == 0 {
parent_version_projects
@@ -308,7 +316,8 @@ pub(crate) async fn fetch(
seconds,
}),
}),
- )?;
+ )
+ .wrap_api_err("executing `ProjectMetrics::Playtime`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_revenue.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_revenue.rs
index 0b292ae0f3..c03de3cf8c 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_revenue.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_revenue.rs
@@ -1,3 +1,4 @@
+use crate::util::error::ApiContext as _;
use futures::StreamExt;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
@@ -95,7 +96,12 @@ pub(crate) async fn fetch(
user_id_bucket_project_ids,
)
.fetch(pool);
- while let Some(row) = rows.next().await.transpose()? {
+ while let Some(row) = rows
+ .next()
+ .await
+ .transpose()
+ .wrap_internal_err("fetching project revenue")?
+ {
let bucket = row
.bucket
.wrap_internal_err("bucket should be non-null - query bug!")?;
@@ -123,7 +129,8 @@ pub(crate) async fn fetch(
revenue,
}),
}),
- )?;
+ )
+ .wrap_api_err("adding project revenue to time slice")?;
}
}
diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_views.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_views.rs
index cd612c3c38..a1b276e99c 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_views.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_views.rs
@@ -1,3 +1,5 @@
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use std::collections::HashMap;
use const_format::formatcp;
@@ -188,10 +190,16 @@ pub(crate) async fn fetch(
query = filter_param.bind(query);
}
- let mut cursor = query.fetch::()?;
+ let mut cursor = query
+ .fetch::()
+ .wrap_internal_err("fetching project-view pagination cursor")?;
let mut buckets = HashMap::::new();
- while let Some(row) = cursor.next().await? {
+ while let Some(row) = cursor
+ .next()
+ .await
+ .wrap_internal_err("fetching project views")?
+ {
let key = ViewBucket {
bucket: row.bucket,
project_id: row.project_id,
@@ -252,7 +260,8 @@ pub(crate) async fn fetch(
views,
}),
}),
- )?;
+ )
+ .wrap_api_err("executing `ProjectMetrics::Views`")?;
}
Ok(())
diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs
index 72d1831dbc..6dc980a1c7 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs
@@ -7,6 +7,9 @@
//! requests, you have to zip together M arrays of N elements
//! - this makes it inconvenient to have separate endpoints
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
+
use xredis::RedisPool;
pub mod facets;
@@ -195,36 +198,30 @@ pub async fn fetch_analytics(
&session_queue,
Scopes::ANALYTICS,
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
let full_time_range = req.time_range.end - req.time_range.start;
if full_time_range < TimeDelta::zero() {
- return Err(ApiError::InvalidInput(
- "End date must be after start date".into(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "End date must be after start date",
+ )));
}
let (num_time_slices, resolution) = match req.time_range.resolution {
TimeRangeResolution::Slices(slices) => {
- let slices = i32::try_from(slices.get()).map_err(|_| {
- ApiError::InvalidInput(
- "Number of slices must fit into an `i32`".into(),
- )
- })?;
+ let slices = i32::try_from(slices.get())
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("number of slices must fit into an `i32`")?;
let resolution = full_time_range / slices;
(slices as usize, resolution)
}
TimeRangeResolution::Minutes(resolution_minutes) => {
let resolution_minutes = i64::try_from(resolution_minutes.get())
- .map_err(|_| {
- ApiError::InvalidInput(
- "Resolution must fit into a `i64`".into(),
- )
- })?;
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("resolution must fit into a `i64`")?;
let resolution = TimeDelta::try_minutes(resolution_minutes)
- .ok_or_else(|| {
- ApiError::InvalidInput("Resolution overflow".into())
- })?;
+ .wrap_request_err_with(|| "resolution overflow")?;
let num_slices =
full_time_range.as_seconds_f64() / resolution.as_seconds_f64();
@@ -248,7 +245,9 @@ pub async fn fetch_analytics(
let project_ids = {
if req.project_ids.is_empty() {
- DBUser::get_projects(user.id.into(), &**pool, &redis).await?
+ DBUser::get_projects(user.id.into(), &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching users from database")?
} else {
req.project_ids
.iter()
@@ -258,7 +257,9 @@ pub async fn fetch_analytics(
};
let project_ids =
- filter_allowed_project_ids(&project_ids, &user, &pool, &redis).await?;
+ filter_allowed_project_ids(&project_ids, &user, &pool, &redis)
+ .await
+ .wrap_api_err("filtering allowed project ids")?;
let project_id_values =
project_ids.iter().map(|id| id.0).collect::>();
@@ -271,7 +272,8 @@ pub async fn fetch_analytics(
&project_id_values,
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err("fetching parent versions from database")?;
let parent_version_ids = parent_versions
.iter()
.map(|version| DBVersionId(version.id))
@@ -281,7 +283,9 @@ pub async fn fetch_analytics(
.map(|version| (DBVersionId(version.id), DBProjectId(version.mod_id)))
.collect::>();
let parent_version_data =
- DBVersion::get_many(&parent_version_ids, &**pool, &redis).await?;
+ DBVersion::get_many(&parent_version_ids, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching versions from database")?;
let visible_version_ids = filter_visible_version_ids(
parent_version_data
.iter()
@@ -291,7 +295,8 @@ pub async fn fetch_analytics(
&pool,
&redis,
)
- .await?;
+ .await
+ .wrap_api_err("filtering visible version ids")?;
let mut project_events = parent_version_data
.iter()
.filter(|version| {
@@ -315,13 +320,15 @@ pub async fn fetch_analytics(
&req.time_range,
&pool,
)
- .await?,
+ .await
+ .wrap_api_err("fetching project status change events")?,
);
project_events.sort_by_key(|event| event.timestamp);
let affiliate_code_ids =
DBAffiliateCode::get_by_affiliate(user.id.into(), &**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching affiliate codes from database")?
.into_iter()
.map(|code| code.id)
.collect::>();
@@ -330,7 +337,6 @@ pub async fn fetch_analytics(
let mut query_clickhouse_cx = QueryClickhouseContext {
clickhouse: &clickhouse,
pool: &pool,
- redis: &redis,
req: &req,
time_slices: &mut time_slices,
project_ids: &project_ids,
@@ -340,12 +346,15 @@ pub async fn fetch_analytics(
};
if let Some(metrics) = &req.return_metrics.project_views {
- metrics::fetch_project_views(&mut query_clickhouse_cx, metrics).await?;
+ metrics::fetch_project_views(&mut query_clickhouse_cx, metrics)
+ .await
+ .wrap_api_err("fetching project views")?;
}
if let Some(metrics) = &req.return_metrics.project_downloads {
metrics::fetch_project_downloads(&mut query_clickhouse_cx, metrics)
- .await?;
+ .await
+ .wrap_api_err("fetching project downloads")?;
}
if let Some(metrics) = &req.return_metrics.project_playtime {
@@ -354,17 +363,21 @@ pub async fn fetch_analytics(
&parent_version_projects,
metrics,
)
- .await?;
+ .await
+ .wrap_api_err("fetching project playtime")?;
}
if let Some(metrics) = &req.return_metrics.affiliate_code_clicks {
metrics::fetch_affiliate_code_clicks(&mut query_clickhouse_cx, metrics)
- .await?;
+ .await
+ .wrap_api_err("fetching affiliate code clicks")?;
}
if let Some(metrics) = &req.return_metrics.project_revenue {
if !scopes.contains(Scopes::PAYOUTS_READ) {
- return Err(AuthenticationError::InvalidCredentials.into());
+ return Err(ApiError::Auth(eyre::eyre!(
+ AuthenticationError::InvalidCredentials
+ )));
}
let user_id_bucket_project_ids = sqlx::query!(
@@ -381,7 +394,8 @@ pub async fn fetch_analytics(
DBUserId::from(user.id).0,
)
.fetch_all(&**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching user IDs from database")?
.into_iter()
.map(|row| row.id)
.collect::>();
@@ -396,7 +410,8 @@ pub async fn fetch_analytics(
user.role.is_mod(),
metrics,
)
- .await?;
+ .await
+ .wrap_api_err("checking mod")?;
}
if let Some(metrics) = &req.return_metrics.affiliate_code_conversions {
@@ -408,12 +423,15 @@ pub async fn fetch_analytics(
num_time_slices,
metrics,
)
- .await?;
+ .await
+ .wrap_api_err("fetching affiliate code conversions")?;
}
if let Some(metrics) = &req.return_metrics.affiliate_code_revenue {
if !scopes.contains(Scopes::PAYOUTS_READ) {
- return Err(AuthenticationError::InvalidCredentials.into());
+ return Err(ApiError::Auth(eyre::eyre!(
+ AuthenticationError::InvalidCredentials
+ )));
}
metrics::fetch_affiliate_code_revenue(
@@ -424,12 +442,17 @@ pub async fn fetch_analytics(
num_time_slices,
metrics,
)
- .await?;
+ .await
+ .wrap_api_err("fetching affiliate code revenue")?;
}
let projects =
- fetch_response_projects(&mut time_slices, &user, &pool, &redis).await?;
- let users = fetch_response_users(&time_slices, &pool, &redis).await?;
+ fetch_response_projects(&mut time_slices, &user, &pool, &redis)
+ .await
+ .wrap_api_err("fetching response projects")?;
+ let users = fetch_response_users(&time_slices, &pool, &redis)
+ .await
+ .wrap_api_err("fetching response users")?;
Ok(web::Json(GetResponse {
metrics: time_slices,
@@ -531,14 +554,17 @@ async fn fetch_response_projects(
}
let project_ids = project_ids.into_iter().collect::>();
- let projects = DBProject::get_many_ids(&project_ids, pool, redis).await?;
+ let projects = DBProject::get_many_ids(&project_ids, pool, redis)
+ .await
+ .wrap_api_err("fetching analytics projects")?;
let visible_project_ids = filter_visible_project_ids(
projects.iter().map(|project| &project.inner).collect(),
&Some(user.clone()),
pool,
false,
)
- .await?
+ .await
+ .wrap_api_err("filtering visible project ids")?
.into_iter()
.collect::>();
@@ -580,7 +606,9 @@ async fn fetch_response_users(
return Ok(HashMap::new());
}
- let users = DBUser::get_many_ids(&user_ids, pool, redis).await?;
+ let users = DBUser::get_many_ids(&user_ids, pool, redis)
+ .await
+ .wrap_internal_err("fetching users from database")?;
Ok(users
.into_iter()
@@ -649,7 +677,10 @@ async fn fetch_project_status_change_events(
time_range.end,
)
.fetch_all(&**pool)
- .await?;
+ .await
+ .wrap_internal_err(
+ "querying database for `fetch_project_status_change_events`",
+ )?;
Ok(rows
.into_iter()
@@ -677,7 +708,6 @@ async fn fetch_project_status_change_events(
pub(crate) struct QueryClickhouseContext<'a> {
pub(crate) clickhouse: &'a clickhouse::Client,
pub(crate) pool: &'a PgPool,
- pub(crate) redis: &'a RedisPool,
pub(crate) req: &'a GetRequest,
pub(crate) time_slices: &'a mut [TimeSlice],
pub(crate) project_ids: &'a [DBProjectId],
@@ -798,14 +828,21 @@ where
for filter_param in filter_params {
query = filter_param.bind(query);
}
- let mut cursor = query.fetch::()?;
+ let mut cursor = query
+ .fetch::()
+ .wrap_internal_err("fetching analytics pagination cursor")?;
- while let Some(row) = cursor.next().await? {
+ while let Some(row) = cursor
+ .next()
+ .await
+ .wrap_internal_err("querying database for `query_clickhouse`")?
+ {
if !row_filter(&row) {
continue;
}
let bucket = row_get_bucket(&row) as usize;
- add_to_time_slice(cx.time_slices, bucket, row_to_analytics(row))?;
+ add_to_time_slice(cx.time_slices, bucket, row_to_analytics(row))
+ .wrap_api_err("executing `row_to_analytics`")?;
}
Ok(())
@@ -827,11 +864,7 @@ pub(crate) fn add_to_time_slice(
};
let num_time_slices = time_slices.len();
- let slice = time_slices.get_mut(bucket).ok_or_else(|| {
- ApiError::InvalidInput(
- format!("bucket {bucket} returned by query out of range for {num_time_slices} - query bug!")
- )
- })?;
+ let slice = time_slices.get_mut(bucket).wrap_request_err_with(|| format!("bucket {bucket} returned by query out of range for {num_time_slices} - query bug!"))?;
slice.0.push(data);
Ok(())
@@ -843,7 +876,9 @@ async fn filter_allowed_project_ids(
pool: &PgPool,
redis: &RedisPool,
) -> Result, ApiError> {
- let projects = DBProject::get_many_ids(project_ids, pool, redis).await?;
+ let projects = DBProject::get_many_ids(project_ids, pool, redis)
+ .await
+ .wrap_api_err("fetching projects for analytics authorization")?;
let team_ids = projects
.iter()
@@ -852,7 +887,8 @@ async fn filter_allowed_project_ids(
let team_members = database::models::DBTeamMember::get_from_team_full_many(
&team_ids, pool, redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching team members from database")?;
let organization_ids = projects
.iter()
@@ -863,7 +899,8 @@ async fn filter_allowed_project_ids(
pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching organizations from database")?;
let organization_team_ids = organizations
.iter()
@@ -875,7 +912,8 @@ async fn filter_allowed_project_ids(
pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching team members from database")?;
Ok(projects
.into_iter()
diff --git a/apps/labrinth/src/routes/v3/analytics_get/old.rs b/apps/labrinth/src/routes/v3/analytics_get/old.rs
index 060a0299d7..a164921783 100644
--- a/apps/labrinth/src/routes/v3/analytics_get/old.rs
+++ b/apps/labrinth/src/routes/v3/analytics_get/old.rs
@@ -1,5 +1,8 @@
//! TODO: this module should be removed; it is superseded by `analytics_get`
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
+
use super::ApiError;
use crate::database;
use crate::database::PgPool;
@@ -13,7 +16,6 @@ use crate::{
use actix_web::{HttpRequest, HttpResponse, get, web};
use ariadne::ids::base62_impl::to_base62;
use chrono::{DateTime, Duration, Utc};
-use eyre::eyre;
use serde::{Deserialize, Serialize};
use sqlx::postgres::types::PgInterval;
use std::collections::HashMap;
@@ -77,13 +79,15 @@ pub async fn playtimes_get(
Scopes::ANALYTICS,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -95,7 +99,9 @@ pub async fn playtimes_get(
// - Filter out unauthorized projects/versions
// - If no project_ids or version_ids are provided, we default to all projects the user has access to
let project_ids =
- filter_allowed_ids(project_ids, user, &pool, &redis, None).await?;
+ filter_allowed_ids(project_ids, user, &pool, &redis, None)
+ .await
+ .wrap_api_err("filtering authorized playtime project IDs")?;
// Get the views
let playtimes = crate::clickhouse::fetch_playtimes(
@@ -105,7 +111,8 @@ pub async fn playtimes_get(
resolution_minutes,
clickhouse.into_inner(),
)
- .await?;
+ .await
+ .wrap_api_err("fetching project playtime from ClickHouse")?;
let mut hm = HashMap::new();
for playtime in playtimes {
@@ -159,13 +166,15 @@ pub async fn views_get(
Scopes::ANALYTICS,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -177,7 +186,9 @@ pub async fn views_get(
// - Filter out unauthorized projects/versions
// - If no project_ids or version_ids are provided, we default to all projects the user has access to
let project_ids =
- filter_allowed_ids(project_ids, user, &pool, &redis, None).await?;
+ filter_allowed_ids(project_ids, user, &pool, &redis, None)
+ .await
+ .wrap_api_err("filtering authorized view project IDs")?;
// Get the views
let views = crate::clickhouse::fetch_views(
@@ -187,7 +198,8 @@ pub async fn views_get(
resolution_minutes,
clickhouse.into_inner(),
)
- .await?;
+ .await
+ .wrap_api_err("fetching project views from ClickHouse")?;
let mut hm = HashMap::new();
for views in views {
@@ -241,13 +253,15 @@ pub async fn downloads_get(
Scopes::ANALYTICS,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -260,7 +274,8 @@ pub async fn downloads_get(
// - If no project_ids or version_ids are provided, we default to all projects the user has access to
let project_ids =
filter_allowed_ids(project_ids, user_option, &pool, &redis, None)
- .await?;
+ .await
+ .wrap_api_err("filtering authorized download project IDs")?;
// Get the downloads
let downloads = crate::clickhouse::fetch_downloads(
@@ -270,7 +285,8 @@ pub async fn downloads_get(
resolution_minutes,
clickhouse.into_inner(),
)
- .await?;
+ .await
+ .wrap_api_err("fetching project downloads from ClickHouse")?;
let mut hm = HashMap::new();
for downloads in downloads {
@@ -323,13 +339,15 @@ pub async fn revenue_get(
Scopes::PAYOUTS_READ,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -357,13 +375,15 @@ pub async fn revenue_get(
&redis,
Some(true),
)
- .await?;
+ .await
+ .wrap_api_err("filtering authorized revenue project IDs")?;
let duration: PgInterval = Duration::minutes(resolution_minutes as i64)
.try_into()
- .map_err(|_| {
- ApiError::Request(eyre!("Invalid `resolution_minutes`"))
- })?;
+ .map_err(|err: Box| {
+ eyre::eyre!("{err}")
+ })
+ .wrap_request_err("invalid `resolution_minutes`")?;
// Get the revenue data
let project_ids = project_ids.unwrap_or_default();
@@ -387,7 +407,7 @@ pub async fn revenue_get(
duration,
)
.fetch_all(&**pool)
- .await?.into_iter().map(|x| PayoutValue {
+ .await.wrap_internal_err("fetching payouts values from database")?.into_iter().map(|x| PayoutValue {
mod_id: x.mod_id,
amount_sum: x.amount_sum,
interval_start: x.interval_start,
@@ -406,7 +426,7 @@ pub async fn revenue_get(
duration,
)
.fetch_all(&**pool)
- .await?.into_iter().map(|x| PayoutValue {
+ .await.wrap_internal_err("querying database for `revenue_get`")?.into_iter().map(|x| PayoutValue {
mod_id: x.mod_id,
amount_sum: x.amount_sum,
interval_start: x.interval_start,
@@ -476,13 +496,15 @@ pub async fn countries_downloads_get(
Scopes::ANALYTICS,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -491,7 +513,11 @@ pub async fn countries_downloads_get(
// - Filter out unauthorized projects/versions
// - If no project_ids or version_ids are provided, we default to all projects the user has access to
let project_ids =
- filter_allowed_ids(project_ids, user, &pool, &redis, None).await?;
+ filter_allowed_ids(project_ids, user, &pool, &redis, None)
+ .await
+ .wrap_api_err(
+ "filtering authorized download-country project IDs",
+ )?;
// Get the countries
let countries = crate::clickhouse::fetch_countries_downloads(
@@ -500,7 +526,8 @@ pub async fn countries_downloads_get(
end_date,
clickhouse.into_inner(),
)
- .await?;
+ .await
+ .wrap_api_err("fetching download countries from ClickHouse")?;
let mut hm = HashMap::new();
for views in countries {
@@ -562,13 +589,15 @@ pub async fn countries_views_get(
Scopes::ANALYTICS,
)
.await
- .map(|x| x.1)?;
+ .map(|x| x.1)
+ .wrap_auth_err("authenticating API request")?;
let project_ids = data
.project_ids
.as_ref()
.map(|ids| serde_json::from_str::>(ids))
- .transpose()?;
+ .transpose()
+ .wrap_request_err("deserializing JSON data")?;
let start_date = data.start_date.unwrap_or(Utc::now() - Duration::weeks(2));
let end_date = data.end_date.unwrap_or(Utc::now());
@@ -577,7 +606,9 @@ pub async fn countries_views_get(
// - Filter out unauthorized projects/versions
// - If no project_ids or version_ids are provided, we default to all projects the user has access to
let project_ids =
- filter_allowed_ids(project_ids, user, &pool, &redis, None).await?;
+ filter_allowed_ids(project_ids, user, &pool, &redis, None)
+ .await
+ .wrap_api_err("filtering authorized view-country project IDs")?;
// Get the countries
let countries = crate::clickhouse::fetch_countries_views(
@@ -586,7 +617,8 @@ pub async fn countries_views_get(
end_date,
clickhouse.into_inner(),
)
- .await?;
+ .await
+ .wrap_api_err("fetching view countries from ClickHouse")?;
let mut hm = HashMap::new();
for views in countries {
@@ -635,7 +667,8 @@ async fn filter_allowed_ids(
if project_ids.is_none() && !remove_defaults.unwrap_or(false) {
project_ids = Some(
user_item::DBUser::get_projects(user.id.into(), &***pool, redis)
- .await?
+ .await
+ .wrap_internal_err("deleting user from database")?
.into_iter()
.map(|x| ProjectId::from(x).to_string())
.collect(),
@@ -650,7 +683,8 @@ async fn filter_allowed_ids(
&***pool,
redis,
)
- .await?;
+ .await
+ .wrap_api_err("fetching analytics projects")?;
let team_ids = projects_data
.iter()
@@ -660,7 +694,8 @@ async fn filter_allowed_ids(
database::models::DBTeamMember::get_from_team_full_many(
&team_ids, &***pool, redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching team members from database")?;
let organization_ids = projects_data
.iter()
@@ -671,7 +706,8 @@ async fn filter_allowed_ids(
&***pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching organizations from database")?;
let organization_team_ids = organizations
.iter()
@@ -683,7 +719,8 @@ async fn filter_allowed_ids(
&***pool,
redis,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching team members from database")?;
let ids = projects_data
.into_iter()
diff --git a/apps/labrinth/src/routes/v3/blocked_users.rs b/apps/labrinth/src/routes/v3/blocked_users.rs
index 4090e3ac1c..2cdd00bbc9 100644
--- a/apps/labrinth/src/routes/v3/blocked_users.rs
+++ b/apps/labrinth/src/routes/v3/blocked_users.rs
@@ -6,6 +6,7 @@ use crate::database::models::friend_item::DBFriend;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, delete, get, post, web};
use ariadne::ids::UserId;
use eyre::eyre;
@@ -34,30 +35,43 @@ pub async fn block_user(
&session_queue,
Scopes::USER_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let user_id = info.into_inner().0;
- let Some(blocked) = DBUser::get(&user_id, &**pool, &redis).await? else {
- return Err(ApiError::NotFound);
+ let Some(blocked) = DBUser::get(&user_id, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching user from database")?
+ else {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
if blocked.id == user.id.into() {
return Err(ApiError::Request(eyre!("you cannot block yourself")));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- DBFriend::remove(user.id.into(), blocked.id, &mut transaction).await?;
+ DBFriend::remove(user.id.into(), blocked.id, &mut transaction)
+ .await
+ .wrap_internal_err("deleting friend from database")?;
DBBlockedUser {
user_id: user.id.into(),
blocked_id: blocked.id,
}
.insert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("inserting database records for `block_user`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(())
}
@@ -79,15 +93,21 @@ pub async fn unblock_user(
&session_queue,
Scopes::USER_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let user_id = info.into_inner().0;
- let Some(blocked) = DBUser::get(&user_id, &**pool, &redis).await? else {
- return Err(ApiError::NotFound);
+ let Some(blocked) = DBUser::get(&user_id, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching user from database")?
+ else {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
- DBBlockedUser::remove(user.id.into(), blocked.id, &**pool).await?;
+ DBBlockedUser::remove(user.id.into(), blocked.id, &**pool)
+ .await
+ .wrap_internal_err("deleting blocked user from database")?;
Ok(())
}
@@ -108,11 +128,13 @@ pub async fn get_blocked_users(
&session_queue,
Scopes::USER_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let blocked = DBBlockedUser::get_blocked_for_user(user.id.into(), &**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching blocked user from database")?
.into_iter()
.map(UserId::from)
.collect();
diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs
index fe549298cf..b5e5d266a8 100644
--- a/apps/labrinth/src/routes/v3/collections.rs
+++ b/apps/labrinth/src/routes/v3/collections.rs
@@ -14,6 +14,7 @@ use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::v3::project_creation::CreateError;
+use crate::util::error::ApiContext as _;
use crate::util::error::Context;
use crate::util::img::delete_old_images;
use crate::util::routes::read_limited_from_payload;
@@ -157,16 +158,20 @@ pub async fn collections_get(
redis: web::Data,
session_queue: web::Data,
) -> Result {
- let ids = serde_json::from_str::>(&ids.ids)?;
+ let ids = serde_json::from_str::>(&ids.ids)
+ .wrap_request_err("deserializing JSON data")?;
let ids = ids
.into_iter()
.map(|x| {
parse_base62(x).map(|x| database::models::DBCollectionId(x as i64))
})
- .collect::, _>>()?;
+ .collect::, _>>()
+ .wrap_request_err("parsing collection IDs")?;
let collections_data =
- database::models::DBCollection::get_many(&ids, &**pool, &redis).await?;
+ database::models::DBCollection::get_many(&ids, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching collections from database")?;
let user_option = get_user_from_headers(
&req,
@@ -181,7 +186,8 @@ pub async fn collections_get(
let collections =
filter_visible_collections(collections_data, &user_option, false)
- .await?;
+ .await
+ .wrap_api_err("filtering visible collections")?;
Ok(HttpResponse::Ok().json(collections))
}
@@ -197,9 +203,13 @@ pub async fn collection_get(
) -> Result {
let string = info.into_inner().0;
- let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
+ let id = database::models::DBCollectionId(
+ parse_base62(&string).wrap_request_err("parsing collection ID")? as i64,
+ );
let collection_data =
- database::models::DBCollection::get(id, &**pool, &redis).await?;
+ database::models::DBCollection::get(id, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching collection from database")?;
let user_option = get_user_from_headers(
&req,
&**pool,
@@ -212,11 +222,13 @@ pub async fn collection_get(
.ok();
if let Some(data) = collection_data
- && is_visible_collection(&data, &user_option, false).await?
+ && is_visible_collection(&data, &user_option, false)
+ .await
+ .wrap_api_err("checking collection visibility")?
{
return Ok(HttpResponse::Ok().json(Collection::from(data)));
}
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
#[derive(Deserialize, Validate, utoipa::ToSchema)]
@@ -256,17 +268,22 @@ pub async fn collection_edit(
&session_queue,
Scopes::COLLECTION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
- new_collection.validate().map_err(|err| {
- ApiError::Validation(validation_errors_to_string(err, None))
- })?;
+ new_collection
+ .validate()
+ .map_err(|err| eyre::eyre!(err))
+ .wrap_request_err("validating request")?;
let string = info.into_inner().0;
- let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
- let result =
- database::models::DBCollection::get(id, &**pool, &redis).await?;
+ let id = database::models::DBCollectionId(
+ parse_base62(&string).wrap_request_err("parsing collection ID")? as i64,
+ );
+ let result = database::models::DBCollection::get(id, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching collection from database")?;
if let Some(collection_item) = result {
if !can_modify_collection(&collection_item, &user) {
@@ -275,7 +292,10 @@ pub async fn collection_edit(
let id = collection_item.id;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if let Some(name) = &new_collection.name {
sqlx::query!(
@@ -288,7 +308,8 @@ pub async fn collection_edit(
id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_edit`")?;
}
if let Some(description) = &new_collection.description {
@@ -302,7 +323,8 @@ pub async fn collection_edit(
id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_edit`")?;
}
if let Some(status) = &new_collection.status {
@@ -310,9 +332,9 @@ pub async fn collection_edit(
|| collection_item.status.is_approved()
&& status.can_be_requested())
{
- return Err(ApiError::CustomAuthentication(
- "You don't have permission to set this status!".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You don't have permission to set this status!",
+ )));
}
sqlx::query!(
@@ -325,7 +347,8 @@ pub async fn collection_edit(
id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_edit`")?;
}
if let Some(new_project_ids) = &new_collection.new_projects {
@@ -338,7 +361,8 @@ pub async fn collection_edit(
collection_item.id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("fetching new project IDs from database")?;
let collection_item_ids = new_project_ids
.iter()
@@ -349,9 +373,10 @@ pub async fn collection_edit(
let project = database::models::DBProject::get(
project_id, &**pool, &redis,
)
- .await?
+ .await
+ .wrap_api_err("fetching project from database")?
.wrap_request_err_with(|| {
- eyre!("The specified project {project_id} does not exist!")
+ eyre!("the specified project `{project_id}` does not exist")
})?;
validated_project_ids.push(project.inner.id.0);
}
@@ -366,7 +391,8 @@ pub async fn collection_edit(
&validated_project_ids[..],
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_edit`")?;
sqlx::query!(
"
@@ -377,16 +403,21 @@ pub async fn collection_edit(
collection_item.id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_edit`")?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::DBCollection::clear_cache(collection_item.id, &redis)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().body(""))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -420,18 +451,20 @@ pub async fn collection_icon_edit(
&session_queue,
Scopes::COLLECTION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let string = info.into_inner().0;
- let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
+ let id = database::models::DBCollectionId(
+ parse_base62(&string).wrap_request_err("parsing collection ID")? as i64,
+ );
let collection_item =
database::models::DBCollection::get(id, &**pool, &redis)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The specified collection does not exist!".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching collection from database")?
+ .wrap_request_err_with(|| {
+ "the specified collection does not exist!".to_string()
})?;
if !can_modify_collection(&collection_item, &user) {
@@ -444,14 +477,16 @@ pub async fn collection_icon_edit(
FileHostPublicity::Public,
&**file_host,
)
- .await?;
+ .await
+ .wrap_api_err("deleting old images")?;
let bytes = read_limited_from_payload(
&mut payload,
262144,
"Icons must be smaller than 256KiB",
)
- .await?;
+ .await
+ .wrap_api_err("executing `read_limited_from_payload`")?;
let collection_id: CollectionId = collection_item.id.into();
let upload_result = crate::util::img::upload_image_optimized(
@@ -463,9 +498,13 @@ pub async fn collection_icon_edit(
Some(1.0),
&**file_host,
)
- .await?;
+ .await
+ .wrap_api_err("uploading image")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
sqlx::query!(
"
@@ -479,11 +518,16 @@ pub async fn collection_icon_edit(
collection_item.id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `collection_icon_edit`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::DBCollection::clear_cache(collection_item.id, &redis)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().body(""))
}
@@ -505,18 +549,20 @@ pub async fn delete_collection_icon(
&session_queue,
Scopes::COLLECTION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let string = info.into_inner().0;
- let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
+ let id = database::models::DBCollectionId(
+ parse_base62(&string).wrap_request_err("parsing collection ID")? as i64,
+ );
let collection_item =
database::models::DBCollection::get(id, &**pool, &redis)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The specified collection does not exist!".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching collection from database")?
+ .wrap_request_err_with(|| {
+ "the specified collection does not exist!".to_string()
})?;
if !can_modify_collection(&collection_item, &user) {
return Ok(HttpResponse::Unauthorized().body(""));
@@ -528,8 +574,12 @@ pub async fn delete_collection_icon(
FileHostPublicity::Public,
&**file_host,
)
- .await?;
- let mut transaction = pool.begin().await?;
+ .await
+ .wrap_api_err("deleting old images")?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
sqlx::query!(
"
@@ -540,11 +590,16 @@ pub async fn delete_collection_icon(
collection_item.id as database::models::ids::DBCollectionId,
)
.execute(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err("querying database for `delete_collection_icon`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
database::models::DBCollection::clear_cache(collection_item.id, &redis)
- .await?;
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
Ok(HttpResponse::NoContent().body(""))
}
@@ -565,37 +620,48 @@ pub async fn collection_delete(
&session_queue,
Scopes::COLLECTION_DELETE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let string = info.into_inner().0;
- let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
+ let id = database::models::DBCollectionId(
+ parse_base62(&string).wrap_request_err("parsing collection ID")? as i64,
+ );
let collection = database::models::DBCollection::get(id, &**pool, &redis)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The specified collection does not exist!".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching collection from database")?
+ .wrap_request_err_with(|| {
+ "the specified collection does not exist!".to_string()
})?;
if !can_modify_collection(&collection, &user) {
return Ok(HttpResponse::Unauthorized().body(""));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let result = database::models::DBCollection::remove(
collection.id,
&mut transaction,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("deleting collection from database")?;
- transaction.commit().await?;
- database::models::DBCollection::clear_cache(collection.id, &redis).await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
+ database::models::DBCollection::clear_cache(collection.id, &redis)
+ .await
+ .wrap_internal_err("clearing cached data from Redis")?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
diff --git a/apps/labrinth/src/routes/v3/content/mod.rs b/apps/labrinth/src/routes/v3/content/mod.rs
index 4d8cda3b91..51379b7f1b 100644
--- a/apps/labrinth/src/routes/v3/content/mod.rs
+++ b/apps/labrinth/src/routes/v3/content/mod.rs
@@ -11,6 +11,7 @@ use crate::models::pats::Scopes;
use crate::models::projects::{DependencyType, Version};
use crate::models::users::User;
use crate::queue::session::AuthQueue;
+use crate::util::error::ApiContext as _;
use actix_web::{HttpRequest, post, web};
use ariadne::ids::base62_impl::parse_base62;
use async_trait::async_trait;
@@ -72,7 +73,8 @@ pub async fn resolve_content(
modrinth_content_management::resolve_content(&mut provider, request)
.await
}
- .map_err(resolve_error_to_api)?;
+ .map_err(resolve_error_to_api)
+ .wrap_api_err("executing `modrinth_content_management::resolve_content`")?;
Ok(web::Json(plan))
}
diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs
index 0f02c081da..5be589a6ac 100644
--- a/apps/labrinth/src/routes/v3/friends.rs
+++ b/apps/labrinth/src/routes/v3/friends.rs
@@ -13,6 +13,8 @@ use crate::routes::internal::statuses::{
};
use crate::sync::friends::RedisFriendsMessage;
use crate::sync::status::get_user_status;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use ariadne::networking::message::ServerToClientMessage;
use chrono::Utc;
@@ -42,41 +44,54 @@ pub async fn add_friend(
&session_queue,
Scopes::USER_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let string = info.into_inner().0;
- let Some(friend) = DBUser::get(&string, &**pool, &redis).await? else {
- return Err(ApiError::NotFound);
+ let Some(friend) = DBUser::get(&string, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching user from database")?
+ else {
+ return Err(ApiError::NotFound(eyre::eyre!("resource not found")));
};
- if DBBlockedUser::is_blocked(friend.id, user.id.into(), &**pool).await? {
- return Err(ApiError::InvalidInput(
- "You've been blocked the other user!".to_string(),
- ));
- } else if DBBlockedUser::is_blocked(user.id.into(), friend.id, &**pool)
- .await?
+ if DBBlockedUser::is_blocked(friend.id, user.id.into(), &**pool)
+ .await
+ .wrap_internal_err("checking whether target user blocked requester")?
{
- return Err(ApiError::InvalidInput(
- "You've blocked the other user!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You've been blocked the other user!",
+ )));
+ } else if DBBlockedUser::is_blocked(user.id.into(), friend.id, &**pool)
+ .await
+ .wrap_internal_err("checking whether requester blocked target user")?
+ {
+ return Err(ApiError::Request(eyre::eyre!(
+ "You've blocked the other user!",
+ )));
}
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
if let Some(friend) =
- DBFriend::get_friend(user.id.into(), friend.id, &**pool).await?
+ DBFriend::get_friend(user.id.into(), friend.id, &**pool)
+ .await
+ .wrap_internal_err("fetching friend from database")?
{
if friend.accepted {
- return Err(ApiError::InvalidInput(
- "You are already friends with this user!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You are already friends with this user!",
+ )));
}
if !friend.accepted && user.id != friend.friend_id.into() {
- return Err(ApiError::InvalidInput(
- "You cannot accept your own friend request!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You cannot accept your own friend request!",
+ )));
}
DBFriend::update_friend(
@@ -85,7 +100,8 @@ pub async fn add_friend(
true,
&mut transaction,
)
- .await?;
+ .await
+ .wrap_internal_err("updating friend in database")?;
async fn send_friend_status(
user_id: DBUserId,
@@ -103,27 +119,30 @@ pub async fn add_friend(
status: friend_status,
},
)
- .await?;
+ .await
+ .wrap_internal_err("updating friend in database")?;
}
Ok(())
}
send_friend_status(friend.user_id, friend.friend_id, &db, &redis)
- .await?;
+ .await
+ .wrap_api_err("executing `send_friend_status`")?;
send_friend_status(friend.friend_id, friend.user_id, &db, &redis)
- .await?;
+ .await
+ .wrap_api_err("executing `send_friend_status`")?;
} else {
if friend.id == user.id.into() {
- return Err(ApiError::InvalidInput(
- "You cannot add yourself as a friend!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "You cannot add yourself as a friend!",
+ )));
}
if !friend.allow_friend_requests {
- return Err(ApiError::InvalidInput(
- "Friend requests are disabled for this user!".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Friend requests are disabled for this user!",
+ )));
}
DBFriend {
@@ -133,17 +152,26 @@ pub async fn add_friend(
accepted: false,
}
.insert(&mut transaction)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `send_friend_status`",
+ )?;
send_message_to_user(
&db,
friend.id.into(),
&ServerToClientMessage::FriendRequest { from: user.id },
)
- .await?;
+ .await
+ .wrap_internal_err(
+ "inserting database records for `send_friend_status`",
+ )?;
}
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().body(""))
}
@@ -166,29 +194,41 @@ pub async fn remove_friend(
&session_queue,
Scopes::USER_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let string = info.into_inner().0;
- let friend = DBUser::get(&string, &**pool, &redis).await?;
+ let friend = DBUser::get(&string, &**pool, &redis)
+ .await
+ .wrap_internal_err("fetching user from database")?;
if let Some(friend) = friend {
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
- DBFriend::remove(user.id.into(), friend.id, &mut transaction).await?;
+ DBFriend::remove(user.id.into(), friend.id, &mut transaction)
+ .await
+ .wrap_internal_err("deleting friend from database")?;
send_message_to_user(
&db,
friend.id.into(),
&ServerToClientMessage::FriendRequestRejected { from: user.id },
)
- .await?;
+ .await
+ .wrap_internal_err("deleting friend from database")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().body(""))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -208,11 +248,13 @@ pub async fn friends(
&session_queue,
Scopes::USER_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let friends = DBFriend::get_user_friends(user.id.into(), None, &**pool)
- .await?
+ .await
+ .wrap_internal_err("fetching friends from database")?
.into_iter()
.map(UserFriend::from)
.collect::>();
diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs
index 6b53cf0fab..7e45da50c1 100644
--- a/apps/labrinth/src/routes/v3/images.rs
+++ b/apps/labrinth/src/routes/v3/images.rs
@@ -11,6 +11,8 @@ use crate::models::ids::{ReportId, ThreadMessageId, VersionId};
use crate::models::images::{Image, ImageContext};
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::ApiContext as _;
+use crate::util::error::Context as _;
use crate::util::img::upload_image_optimized;
use crate::util::routes::read_limited_from_payload;
use actix_web::{HttpRequest, HttpResponse, post, web};
@@ -68,7 +70,8 @@ pub async fn images_add(
&session_queue,
context.relevant_scope(),
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
// Attempt to associated a supplied id with the context
@@ -77,25 +80,28 @@ pub async fn images_add(
ImageContext::Project { project_id } => {
if let Some(id) = data.project_id {
let project =
- project_item::DBProject::get(&id, &**pool, &redis).await?;
+ project_item::DBProject::get(&id, &**pool, &redis)
+ .await
+ .wrap_api_err("fetching project from database")?;
if let Some(project) = project {
if is_team_member_project(
&project.inner,
&Some(user.clone()),
&pool,
)
- .await?
+ .await
+ .wrap_api_err("checking team member project")?
{
*project_id = Some(project.inner.id.into());
} else {
- return Err(ApiError::CustomAuthentication(
- "You are not authorized to upload images for this project".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to upload images for this project",
+ )));
}
} else {
- return Err(ApiError::InvalidInput(
- "The project could not be found.".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "The project could not be found.",
+ )));
}
}
}
@@ -103,7 +109,8 @@ pub async fn images_add(
if let Some(id) = data.version_id {
let version =
version_item::DBVersion::get(id.into(), &**pool, &redis)
- .await?;
+ .await
+ .wrap_internal_err("fetching version from database")?;
if let Some(version) = version {
if is_team_member_version(
&version.inner,
@@ -111,18 +118,19 @@ pub async fn images_add(
&pool,
&redis,
)
- .await?
+ .await
+ .wrap_api_err("checking team member version")?
{
*version_id = Some(version.inner.id.into());
} else {
- return Err(ApiError::CustomAuthentication(
- "You are not authorized to upload images for this version".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to upload images for this version",
+ )));
}
} else {
- return Err(ApiError::InvalidInput(
- "The version could not be found.".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "The version could not be found.",
+ )));
}
}
}
@@ -130,60 +138,63 @@ pub async fn images_add(
if let Some(id) = data.thread_message_id {
let thread_message =
thread_item::DBThreadMessage::get(id.into(), &**pool)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The thread message could not found."
- .to_string(),
- )
+ .await
+ .wrap_internal_err(
+ "fetching thread message from database",
+ )?
+ .wrap_request_err_with(|| {
+ "the thread message could not found.".to_string()
})?;
let thread = thread_item::DBThread::get(thread_message.thread_id, &**pool)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The thread associated with the thread message could not be found"
- .to_string(),
- )
- })?;
- if is_authorized_thread(&thread, &user, &pool).await? {
+ .await.wrap_internal_err("fetching thread from database")?
+ .wrap_request_err_with(|| "the thread associated with the thread message could not be found"
+ .to_string())?;
+ if is_authorized_thread(&thread, &user, &pool)
+ .await
+ .wrap_api_err("checking thread authorization")?
+ {
*thread_message_id = Some(thread_message.id.into());
} else {
- return Err(ApiError::CustomAuthentication(
- "You are not authorized to upload images for this thread message"
- .to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to upload images for this thread message",
+ )));
}
}
}
ImageContext::Report { report_id } => {
if let Some(id) = data.report_id {
let report = report_item::DBReport::get(id.into(), &**pool)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The report could not be found.".to_string(),
- )
+ .await
+ .wrap_internal_err("fetching report from database")?
+ .wrap_request_err_with(|| {
+ "the report could not be found.".to_string()
})?;
- let thread = thread_item::DBThread::get(report.thread_id, &**pool)
- .await?
- .ok_or_else(|| {
- ApiError::InvalidInput(
- "The thread associated with the report could not be found.".to_string(),
- )
- })?;
- if is_authorized_thread(&thread, &user, &pool).await? {
+ let thread = thread_item::DBThread::get(
+ report.thread_id,
+ &**pool,
+ )
+ .await
+ .wrap_internal_err("fetching thread from database")?
+ .wrap_request_err_with(|| {
+ "the thread associated with the report could not be found."
+ .to_string()
+ })?;
+ if is_authorized_thread(&thread, &user, &pool)
+ .await
+ .wrap_api_err("checking thread authorization")?
+ {
*report_id = Some(report.id.into());
} else {
- return Err(ApiError::CustomAuthentication(
- "You are not authorized to upload images for this report".to_string(),
- ));
+ return Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to upload images for this report",
+ )));
}
}
}
ImageContext::Unknown => {
- return Err(ApiError::InvalidInput(
- "Context must be one of: project, version, thread_message, report".to_string(),
- ));
+ return Err(ApiError::Request(eyre::eyre!(
+ "Context must be one of: project, version, thread_message, report",
+ )));
}
}
@@ -193,7 +204,8 @@ pub async fn images_add(
1_048_576,
"Icons must be smaller than 1MiB",
)
- .await?;
+ .await
+ .wrap_api_err("executing `read_limited_from_payload`")?;
let content_length = bytes.len();
let upload_result = upload_image_optimized(
@@ -205,12 +217,18 @@ pub async fn images_add(
None,
&**file_host,
)
- .await?;
+ .await
+ .wrap_api_err("uploading image")?;
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
let db_image: database::models::DBImage = database::models::DBImage {
- id: database::models::generate_image_id(&mut transaction).await?,
+ id: database::models::generate_image_id(&mut transaction)
+ .await
+ .wrap_internal_err("generating image ID")?,
url: upload_result.url,
raw_url: upload_result.raw_url,
size: content_length as u64,
@@ -252,7 +270,10 @@ pub async fn images_add(
};
// Insert
- db_image.insert(&mut transaction).await?;
+ db_image
+ .insert(&mut transaction)
+ .await
+ .wrap_internal_err("inserting database records for `images_add`")?;
let image = Image {
id: db_image.id.into(),
@@ -263,7 +284,10 @@ pub async fn images_add(
context,
};
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::Ok().json(image))
}
diff --git a/apps/labrinth/src/routes/v3/limits.rs b/apps/labrinth/src/routes/v3/limits.rs
index f7ef89a48f..ab11d37a8e 100644
--- a/apps/labrinth/src/routes/v3/limits.rs
+++ b/apps/labrinth/src/routes/v3/limits.rs
@@ -1,4 +1,5 @@
use crate::database::PgPool;
+use crate::util::error::Context as _;
use crate::{
auth::get_user_from_headers,
models::{pats::Scopes, v3::user_limits::UserLimits},
@@ -29,9 +30,12 @@ pub async fn get_project_limits(
&session_queue,
Scopes::empty(),
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let limits = UserLimits::get_for_projects(&user, &pool).await?;
+ let limits = UserLimits::get_for_projects(&user, &pool)
+ .await
+ .wrap_internal_err("fetching user limits from Redis")?;
Ok(web::Json(limits))
}
@@ -50,9 +54,12 @@ pub async fn get_organization_limits(
&session_queue,
Scopes::empty(),
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let limits = UserLimits::get_for_organizations(&user, &pool).await?;
+ let limits = UserLimits::get_for_organizations(&user, &pool)
+ .await
+ .wrap_internal_err("fetching user limits from Redis")?;
Ok(web::Json(limits))
}
@@ -71,8 +78,11 @@ pub async fn get_collection_limits(
&session_queue,
Scopes::empty(),
)
- .await?;
+ .await
+ .wrap_auth_err("authenticating API request")?;
- let limits = UserLimits::get_for_collections(&user, &pool).await?;
+ let limits = UserLimits::get_for_collections(&user, &pool)
+ .await
+ .wrap_internal_err("fetching user limits from Redis")?;
Ok(web::Json(limits))
}
diff --git a/apps/labrinth/src/routes/v3/notifications.rs b/apps/labrinth/src/routes/v3/notifications.rs
index bab2519802..1daa7acf3e 100644
--- a/apps/labrinth/src/routes/v3/notifications.rs
+++ b/apps/labrinth/src/routes/v3/notifications.rs
@@ -6,6 +6,7 @@ use crate::models::notifications::Notification;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
+use crate::util::error::Context as _;
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::{Deserialize, Serialize};
use xredis::RedisPool;
@@ -54,14 +55,16 @@ pub async fn notifications_get(
&session_queue,
Scopes::NOTIFICATION_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
use database::models::DBNotificationId;
use database::models::notification_item::DBNotification;
let notification_ids: Vec =
- serde_json::from_str::>(ids.ids.as_str())?
+ serde_json::from_str::>(ids.ids.as_str())
+ .wrap_request_err("deserializing JSON data")?
.into_iter()
.map(DBNotificationId::from)
.collect();
@@ -71,7 +74,8 @@ pub async fn notifications_get(
¬ification_ids,
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching notifications from database")?;
let notifications: Vec = notifications_data
.into_iter()
@@ -108,7 +112,8 @@ pub async fn notification_get(
&session_queue,
Scopes::NOTIFICATION_READ,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = info.into_inner().0;
@@ -118,16 +123,17 @@ pub async fn notification_get(
id.into(),
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching notification from database")?;
if let Some(data) = notification_data {
if user.id == data.user_id.into() || user.role.is_admin() {
Ok(HttpResponse::Ok().json(Notification::from(data)))
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -157,7 +163,8 @@ pub async fn notification_read(
&session_queue,
Scopes::NOTIFICATION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = info.into_inner().0;
@@ -167,29 +174,37 @@ pub async fn notification_read(
id.into(),
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching notification from database")?;
if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_admin() {
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
database::models::notification_item::DBNotification::read(
id.into(),
&mut transaction,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("executing `DBNotification::read`")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().body(""))
} else {
- Err(ApiError::CustomAuthentication(
- "You are not authorized to read this notification!".to_string(),
- ))
+ Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to read this notification!",
+ )))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -219,7 +234,8 @@ pub async fn notification_delete(
&session_queue,
Scopes::NOTIFICATION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let id = info.into_inner().0;
@@ -229,30 +245,37 @@ pub async fn notification_delete(
id.into(),
&**pool,
)
- .await?;
+ .await
+ .wrap_internal_err("fetching notification from database")?;
if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_admin() {
- let mut transaction = pool.begin().await?;
+ let mut transaction = pool
+ .begin()
+ .await
+ .wrap_internal_err("starting database transaction")?;
database::models::notification_item::DBNotification::remove(
id.into(),
&mut transaction,
&redis,
)
- .await?;
+ .await
+ .wrap_internal_err("deleting notification from database")?;
- transaction.commit().await?;
+ transaction
+ .commit()
+ .await
+ .wrap_internal_err("committing database transaction")?;
Ok(HttpResponse::NoContent().body(""))
} else {
- Err(ApiError::CustomAuthentication(
- "You are not authorized to delete this notification!"
- .to_string(),
- ))
+ Err(ApiError::Auth(eyre::eyre!(
+ "You are not authorized to delete this notification!",
+ )))
}
} else {
- Err(ApiError::NotFound)
+ Err(ApiError::NotFound(eyre::eyre!("resource not found")))
}
}
@@ -286,23 +309,29 @@ pub async fn notifications_read(
&session_queue,
Scopes::NOTIFICATION_WRITE,
)
- .await?
+ .await
+ .wrap_auth_err("authenticating API request")?
.1;
let notification_ids =
- serde_json::from_str::