Staff support dashboard routes (#3160)
* Staff support dashboard routes * Fix clippy
This commit is contained in:
parent
d7814e115d
commit
75b357a069
210
apps/frontend/src/pages/admin/billing/[id].vue
Normal file
210
apps/frontend/src/pages/admin/billing/[id].vue
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="refundModal">
|
||||||
|
<template #title>
|
||||||
|
<span class="text-lg font-extrabold text-contrast">Refund charge</span>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="visibility" class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-semibold text-contrast">
|
||||||
|
Refund type
|
||||||
|
<span class="text-brand-red">*</span>
|
||||||
|
</span>
|
||||||
|
<span> The type of refund to issue. </span>
|
||||||
|
</label>
|
||||||
|
<DropdownSelect
|
||||||
|
id="refund-type"
|
||||||
|
v-model="refundType"
|
||||||
|
:options="refundTypes"
|
||||||
|
name="Refund type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="refundType === 'partial'" class="flex flex-col gap-2">
|
||||||
|
<label for="amount" class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-semibold text-contrast">
|
||||||
|
Amount
|
||||||
|
<span class="text-brand-red">*</span>
|
||||||
|
</span>
|
||||||
|
<span> Enter the amount in cents of USD. For example for $2, enter 200. </span>
|
||||||
|
</label>
|
||||||
|
<input id="amount" v-model="refundAmount" type="number" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="unprovision" class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-semibold text-contrast">
|
||||||
|
Unprovision
|
||||||
|
<span class="text-brand-red">*</span>
|
||||||
|
</span>
|
||||||
|
<span> Whether or not the subscription should be unprovisioned on refund. </span>
|
||||||
|
</label>
|
||||||
|
<Toggle
|
||||||
|
id="unprovision"
|
||||||
|
:model-value="unprovision"
|
||||||
|
:checked="unprovision"
|
||||||
|
@update:model-value="() => (unprovision = !unprovision)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="refundCharge">
|
||||||
|
<CheckIcon aria-hidden="true" />
|
||||||
|
Refund charge
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="refundModal.hide()">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
<div class="normal-page no-sidebar">
|
||||||
|
<h1>{{ user.username }}'s subscriptions</h1>
|
||||||
|
<div class="normal-page__content">
|
||||||
|
<div v-for="subscription in subscriptionCharges" :key="subscription.id" class="card">
|
||||||
|
<span class="font-extrabold text-contrast">
|
||||||
|
<template v-if="subscription.product.metadata.type === 'midas'"> Modrinth Plus </template>
|
||||||
|
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
||||||
|
Modrinth Servers
|
||||||
|
</template>
|
||||||
|
<template v-else> Unknown product </template>
|
||||||
|
<template v-if="subscription.interval">
|
||||||
|
{{ subscription.interval }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<div class="mb-4 mt-2 flex items-center gap-1">
|
||||||
|
{{ subscription.status }} ⋅ {{ $dayjs(subscription.created).format("YYYY-MM-DD") }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="charge in subscription.charges"
|
||||||
|
:key="charge.id"
|
||||||
|
class="universal-card recessed flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Badge
|
||||||
|
:color="charge.status === 'succeeded' ? 'green' : 'red'"
|
||||||
|
:type="charge.status"
|
||||||
|
/>
|
||||||
|
⋅
|
||||||
|
{{ $dayjs(charge.due).format("YYYY-MM-DD") }}
|
||||||
|
⋅
|
||||||
|
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
||||||
|
<template v-if="subscription.interval"> ⋅ {{ subscription.interval }} </template>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="charge.status === 'succeeded'"
|
||||||
|
class="btn"
|
||||||
|
@click="showRefundModal(charge)"
|
||||||
|
>
|
||||||
|
Refund charge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Badge, NewModal, ButtonStyled, DropdownSelect, Toggle } from "@modrinth/ui";
|
||||||
|
import { formatPrice } from "@modrinth/utils";
|
||||||
|
import { CheckIcon, XIcon } from "@modrinth/assets";
|
||||||
|
import { products } from "~/generated/state.json";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const data = useNuxtApp();
|
||||||
|
const vintl = useVIntl();
|
||||||
|
const { formatMessage } = vintl;
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
userNotFoundError: {
|
||||||
|
id: "admin.billing.error.not-found",
|
||||||
|
defaultMessage: "User not found",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: user } = await useAsyncData(`user/${route.params.id}`, () =>
|
||||||
|
useBaseFetch(`user/${route.params.id}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user.value) {
|
||||||
|
throw createError({
|
||||||
|
fatal: true,
|
||||||
|
statusCode: 404,
|
||||||
|
message: formatMessage(messages.userNotFoundError),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscriptions, charges;
|
||||||
|
try {
|
||||||
|
[{ data: subscriptions }, { data: charges }] = await Promise.all([
|
||||||
|
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
|
||||||
|
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
|
||||||
|
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
throw createError({
|
||||||
|
fatal: true,
|
||||||
|
statusCode: 404,
|
||||||
|
message: formatMessage(messages.userNotFoundError),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionCharges = computed(() => {
|
||||||
|
return subscriptions.value.map((subscription) => {
|
||||||
|
return {
|
||||||
|
...subscription,
|
||||||
|
charges: charges.value.filter((charge) => charge.subscription_id === subscription.id),
|
||||||
|
product: products.find((product) =>
|
||||||
|
product.prices.some((price) => price.id === subscription.price_id),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const refundModal = ref();
|
||||||
|
const selectedCharge = ref(null);
|
||||||
|
const refundType = ref("full");
|
||||||
|
const refundTypes = ref(["full", "partial"]);
|
||||||
|
const refundAmount = ref(0);
|
||||||
|
const unprovision = ref(false);
|
||||||
|
|
||||||
|
function showRefundModal(charge) {
|
||||||
|
selectedCharge.value = charge;
|
||||||
|
refundType.value = "full";
|
||||||
|
refundAmount.value = 0;
|
||||||
|
unprovision.value = false;
|
||||||
|
refundModal.value.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refundCharge() {
|
||||||
|
try {
|
||||||
|
await useBaseFetch(`billing/charge/${selectedCharge.value.id}/refund`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: refundType.value,
|
||||||
|
amount: refundAmount.value,
|
||||||
|
unprovision: unprovision.value,
|
||||||
|
}),
|
||||||
|
internal: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
data.$notify({
|
||||||
|
group: "main",
|
||||||
|
title: "Error resubscribing",
|
||||||
|
text: err.message ?? (err.data ? err.data.description : err),
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
⋅
|
⋅
|
||||||
<span>{{ formatPrice(charge.amount, charge.currency_code) }}</span>
|
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
|
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
|
||||||
@ -39,6 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, Badge } from "@modrinth/ui";
|
import { Breadcrumbs, Badge } from "@modrinth/ui";
|
||||||
|
import { formatPrice } from "@modrinth/utils";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@ -66,19 +67,4 @@ const { data: charges } = await useAsyncData(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO move to omorphia utils , duplicated from index
|
|
||||||
function formatPrice(price, currency) {
|
|
||||||
const formatter = new Intl.NumberFormat(vintl.locale, {
|
|
||||||
style: "currency",
|
|
||||||
currency,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxDigits = formatter.resolvedOptions().maximumFractionDigits;
|
|
||||||
|
|
||||||
const convertedPrice = price / Math.pow(10, maxDigits);
|
|
||||||
|
|
||||||
return formatter.format(convertedPrice);
|
|
||||||
}
|
|
||||||
console.log(charges);
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -2,6 +2,57 @@
|
|||||||
<div v-if="user" class="experimental-styles-within">
|
<div v-if="user" class="experimental-styles-within">
|
||||||
<ModalCreation ref="modal_creation" />
|
<ModalCreation ref="modal_creation" />
|
||||||
<CollectionCreateModal ref="modal_collection_creation" />
|
<CollectionCreateModal ref="modal_collection_creation" />
|
||||||
|
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-bold text-primary">Email</span>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
v-tooltip="user.email_verified ? 'Email verified' : 'Email not verified'"
|
||||||
|
class="flex w-fit items-center gap-1"
|
||||||
|
>
|
||||||
|
<span>{{ user.email }}</span>
|
||||||
|
<CheckIcon v-if="user.email_verified" class="h-4 w-4 text-brand" />
|
||||||
|
<XIcon v-else class="h-4 w-4 text-red" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-bold text-primary"> Auth providers </span>
|
||||||
|
<span>{{ user.auth_providers.join(", ") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-bold text-primary"> Payment methods</span>
|
||||||
|
<span>
|
||||||
|
<template v-if="user.payout_data?.paypal_address">
|
||||||
|
Paypal ({{ user.payout_data.paypal_address }} - {{ user.payout_data.paypal_country }})
|
||||||
|
</template>
|
||||||
|
<template v-if="user.payout_data?.paypal_address && user.payout_data?.venmo_address">
|
||||||
|
,
|
||||||
|
</template>
|
||||||
|
<template v-if="user.payout_data?.venmo_address">
|
||||||
|
Venmo ({{ user.payout_data.venmo_address }})
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-bold text-primary"> Has password </span>
|
||||||
|
<span>
|
||||||
|
{{ user.has_password ? "Yes" : "No" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg font-bold text-primary"> Has TOTP </span>
|
||||||
|
<span>
|
||||||
|
{{ user.has_totp ? "Yes" : "No" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||||
<div class="normal-page__header py-4">
|
<div class="normal-page__header py-4">
|
||||||
<ContentPageHeader>
|
<ContentPageHeader>
|
||||||
@ -74,6 +125,16 @@
|
|||||||
shown: auth.user?.id !== user.id,
|
shown: auth.user?.id !== user.id,
|
||||||
},
|
},
|
||||||
{ id: 'copy-id', action: () => copyId() },
|
{ id: 'copy-id', action: () => copyId() },
|
||||||
|
{
|
||||||
|
id: 'open-billing',
|
||||||
|
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||||
|
shown: auth.user && isStaff(auth.user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-info',
|
||||||
|
action: () => $refs.userDetailsModal.show(),
|
||||||
|
shown: auth.user && isStaff(auth.user),
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
>
|
>
|
||||||
@ -90,6 +151,14 @@
|
|||||||
<ClipboardCopyIcon aria-hidden="true" />
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template #open-billing>
|
||||||
|
<CurrencyIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.billingButton) }}
|
||||||
|
</template>
|
||||||
|
<template #open-info>
|
||||||
|
<InfoIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.infoButton) }}
|
||||||
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
@ -264,8 +333,18 @@ import {
|
|||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
|
CurrencyIcon,
|
||||||
|
InfoIcon,
|
||||||
|
CheckIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { OverflowMenu, ButtonStyled, ContentPageHeader, commonMessages } from "@modrinth/ui";
|
import {
|
||||||
|
OverflowMenu,
|
||||||
|
ButtonStyled,
|
||||||
|
ContentPageHeader,
|
||||||
|
commonMessages,
|
||||||
|
NewModal,
|
||||||
|
} from "@modrinth/ui";
|
||||||
|
import { isStaff } from "~/helpers/users.js";
|
||||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||||
import { reportUser } from "~/utils/report-helpers.ts";
|
import { reportUser } from "~/utils/report-helpers.ts";
|
||||||
@ -367,6 +446,14 @@ const messages = defineMessages({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
|
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
|
||||||
},
|
},
|
||||||
|
billingButton: {
|
||||||
|
id: "profile.button.billing",
|
||||||
|
defaultMessage: "Manage user billing",
|
||||||
|
},
|
||||||
|
infoButton: {
|
||||||
|
id: "profile.button.info",
|
||||||
|
defaultMessage: "View user details",
|
||||||
|
},
|
||||||
userNotFoundError: {
|
userNotFoundError: {
|
||||||
id: "profile.error.not-found",
|
id: "profile.error.not-found",
|
||||||
defaultMessage: "User not found",
|
defaultMessage: "User not found",
|
||||||
|
|||||||
@ -757,12 +757,7 @@ impl VersionField {
|
|||||||
l.field_id.0,
|
l.field_id.0,
|
||||||
l.version_id.0,
|
l.version_id.0,
|
||||||
l.int_value,
|
l.int_value,
|
||||||
if let Some(enum_value) = l.enum_value.as_ref().map(|e| e.0)
|
l.enum_value.as_ref().map(|e| e.0).unwrap_or(-1),
|
||||||
{
|
|
||||||
enum_value
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
},
|
|
||||||
l.string_value.clone(),
|
l.string_value.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -83,12 +83,18 @@ pub async fn products(
|
|||||||
Ok(HttpResponse::Ok().json(products))
|
Ok(HttpResponse::Ok().json(products))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SubscriptionsQuery {
|
||||||
|
pub user_id: Option<crate::models::ids::UserId>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("subscriptions")]
|
#[get("subscriptions")]
|
||||||
pub async fn subscriptions(
|
pub async fn subscriptions(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
query: web::Query<SubscriptionsQuery>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(
|
let user = get_user_from_headers(
|
||||||
&req,
|
&req,
|
||||||
@ -102,7 +108,18 @@ pub async fn subscriptions(
|
|||||||
|
|
||||||
let subscriptions =
|
let subscriptions =
|
||||||
user_subscription_item::UserSubscriptionItem::get_all_user(
|
user_subscription_item::UserSubscriptionItem::get_all_user(
|
||||||
user.id.into(),
|
if let Some(user_id) = query.user_id {
|
||||||
|
if user.role.is_admin() {
|
||||||
|
user_id.into()
|
||||||
|
} else {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"You cannot see the subscriptions of other users!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.id.into()
|
||||||
|
},
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@ -573,12 +590,18 @@ pub async fn user_customer(
|
|||||||
Ok(HttpResponse::Ok().json(customer))
|
Ok(HttpResponse::Ok().json(customer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ChargesQuery {
|
||||||
|
pub user_id: Option<crate::models::ids::UserId>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("payments")]
|
#[get("payments")]
|
||||||
pub async fn charges(
|
pub async fn charges(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
query: web::Query<ChargesQuery>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user = get_user_from_headers(
|
let user = get_user_from_headers(
|
||||||
&req,
|
&req,
|
||||||
@ -592,7 +615,18 @@ pub async fn charges(
|
|||||||
|
|
||||||
let charges =
|
let charges =
|
||||||
crate::database::models::charge_item::ChargeItem::get_from_user(
|
crate::database::models::charge_item::ChargeItem::get_from_user(
|
||||||
user.id.into(),
|
if let Some(user_id) = query.user_id {
|
||||||
|
if user.role.is_admin() {
|
||||||
|
user_id.into()
|
||||||
|
} else {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"You cannot see the subscriptions of other users!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.id.into()
|
||||||
|
},
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@ -85,11 +85,13 @@ pub async fn users_get(
|
|||||||
|
|
||||||
#[get("{id}")]
|
#[get("{id}")]
|
||||||
pub async fn user_get(
|
pub async fn user_get(
|
||||||
|
req: HttpRequest,
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let response = v3::users::user_get(info, pool, redis)
|
let response = v3::users::user_get(req, info, pool, redis, session_queue)
|
||||||
.await
|
.await
|
||||||
.or_else(v2_reroute::flatten_404_error)?;
|
.or_else(v2_reroute::flatten_404_error)?;
|
||||||
|
|
||||||
|
|||||||
@ -86,8 +86,6 @@ pub enum CreateError {
|
|||||||
CustomAuthenticationError(String),
|
CustomAuthenticationError(String),
|
||||||
#[error("Image Parsing Error: {0}")]
|
#[error("Image Parsing Error: {0}")]
|
||||||
ImageError(#[from] ImageError),
|
ImageError(#[from] ImageError),
|
||||||
#[error("Reroute Error: {0}")]
|
|
||||||
RerouteError(#[from] reqwest::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for CreateError {
|
impl actix_web::ResponseError for CreateError {
|
||||||
@ -119,7 +117,6 @@ impl actix_web::ResponseError for CreateError {
|
|||||||
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
|
||||||
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
|
||||||
CreateError::ImageError(..) => StatusCode::BAD_REQUEST,
|
CreateError::ImageError(..) => StatusCode::BAD_REQUEST,
|
||||||
CreateError::RerouteError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +143,6 @@ impl actix_web::ResponseError for CreateError {
|
|||||||
CreateError::ValidationError(..) => "invalid_input",
|
CreateError::ValidationError(..) => "invalid_input",
|
||||||
CreateError::FileValidationError(..) => "invalid_input",
|
CreateError::FileValidationError(..) => "invalid_input",
|
||||||
CreateError::ImageError(..) => "invalid_image",
|
CreateError::ImageError(..) => "invalid_image",
|
||||||
CreateError::RerouteError(..) => "reroute_error",
|
|
||||||
},
|
},
|
||||||
description: self.to_string(),
|
description: self.to_string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -128,14 +128,33 @@ pub async fn users_get(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_get(
|
pub async fn user_get(
|
||||||
|
req: HttpRequest,
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let user_data = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
let user_data = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||||
|
|
||||||
if let Some(data) = user_data {
|
if let Some(data) = user_data {
|
||||||
let response: crate::models::users::User = data.into();
|
let auth_user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::SESSION_ACCESS]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|x| x.1)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let response: crate::models::users::User =
|
||||||
|
if auth_user.map(|x| x.role.is_admin()).unwrap_or(false) {
|
||||||
|
crate::models::users::User::from_full(data)
|
||||||
|
} else {
|
||||||
|
data.into()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(response))
|
Ok(HttpResponse::Ok().json(response))
|
||||||
} else {
|
} else {
|
||||||
Err(ApiError::NotFound)
|
Err(ApiError::NotFound)
|
||||||
|
|||||||
@ -985,7 +985,7 @@ pub async fn upload_file(
|
|||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let delphi_url = dotenvy::var("DELPHI_URL")?;
|
let delphi_url = dotenvy::var("DELPHI_URL")?;
|
||||||
let res = client
|
match client
|
||||||
.post(delphi_url)
|
.post(delphi_url)
|
||||||
.json(&serde_json::json!({
|
.json(&serde_json::json!({
|
||||||
"url": url,
|
"url": url,
|
||||||
@ -993,11 +993,17 @@ pub async fn upload_file(
|
|||||||
"version_id": version_id,
|
"version_id": version_id,
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => {
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
error!("Failed to upload file to Delphi: {url}");
|
error!("Failed to upload file to Delphi: {url}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to upload file to Delphi: {url}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
version_files.push(VersionFileBuilder {
|
version_files.push(VersionFileBuilder {
|
||||||
filename: file_name.to_string(),
|
filename: file_name.to_string(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user