develop-updateui #1
6
components.d.ts
vendored
6
components.d.ts
vendored
@@ -37,6 +37,7 @@ declare module 'vue' {
|
||||
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||
ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
||||
CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
||||
copy: typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||
Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
||||
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||
@@ -79,7 +80,9 @@ declare module 'vue' {
|
||||
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||
User2: typeof import('./src/components/icons/User2.vue')['default']
|
||||
UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
||||
'UserIcon copy': typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||
Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
||||
VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
||||
@@ -119,6 +122,7 @@ declare global {
|
||||
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||
const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
||||
const CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
||||
const copy: typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||
const Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
||||
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||
@@ -161,7 +165,9 @@ declare global {
|
||||
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
const UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||
const User2: typeof import('./src/components/icons/User2.vue')['default']
|
||||
const UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
||||
const 'UserIcon copy': typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||
const Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
||||
const VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
||||
|
||||
@@ -26,7 +26,12 @@ export const client = proxyTinyRpc<RpcRoutes>({
|
||||
return await httpClientAdapter({
|
||||
url: `${url}${targetEndpoint}`,
|
||||
pathsForGET: ["health"],
|
||||
JSON: clientJSON,
|
||||
JSON: {
|
||||
// parse: clientJSON.parse,
|
||||
parse: (v, fn) => JSON.parse(v),
|
||||
// stringify: clientJSON.stringify,
|
||||
stringify: (v, fn) => JSON.stringify(v),
|
||||
},
|
||||
headers: () => Promise.resolve({})
|
||||
}).send(data);
|
||||
},
|
||||
|
||||
10
src/components/icons/UserIcon copy.vue
Normal file
10
src/components/icons/UserIcon copy.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
filled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
@@ -1,3 +1,4 @@
|
||||
import { client } from '@/api/rpcclient';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export interface QueueItem {
|
||||
@@ -282,22 +283,19 @@ export function useUploadQueue() {
|
||||
if (!item.file || !item.uploadedUrls) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/merge', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
filename: item.file.name,
|
||||
chunks: item.uploadedUrls,
|
||||
size: item.file.size
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Merge failed');
|
||||
const data = await client.merge(item.file.name, item.uploadedUrls, item.file.size);
|
||||
// const response = await fetch('/merge', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({
|
||||
// filename: item.file.name,
|
||||
// chunks: item.uploadedUrls,
|
||||
// size: item.file.size
|
||||
// })
|
||||
// });
|
||||
if (!data) {
|
||||
throw new Error('No response from server');
|
||||
}
|
||||
|
||||
item.status = 'complete';
|
||||
item.progress = 100;
|
||||
item.uploaded = item.total;
|
||||
|
||||
@@ -142,24 +142,26 @@ const confirmTwoFactor = async () => {
|
||||
};
|
||||
|
||||
const connectTelegram = async () => {
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
telegramConnected.value = true;
|
||||
telegramUsername.value = '@telegram_user';
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.securityConnected.toast.telegramConnectedSummary'),
|
||||
detail: t('settings.securityConnected.toast.telegramConnectedDetail', { username: telegramUsername.value }),
|
||||
life: 3000
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('settings.securityConnected.toast.telegramConnectFailedSummary'),
|
||||
detail: t('settings.securityConnected.toast.telegramConnectFailedDetail'),
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
// https://t.me/<username_bot>?start=abc123
|
||||
window.open(`https://t.me/hlstiktok_bot?start=${auth.user?.username}`, "_blank");
|
||||
// try {
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// telegramConnected.value = true;
|
||||
// telegramUsername.value = '@telegram_user';
|
||||
// toast.add({
|
||||
// severity: 'success',
|
||||
// summary: t('settings.securityConnected.toast.telegramConnectedSummary'),
|
||||
// detail: t('settings.securityConnected.toast.telegramConnectedDetail', { username: telegramUsername.value }),
|
||||
// life: 3000
|
||||
// });
|
||||
// } catch (e) {
|
||||
// toast.add({
|
||||
// severity: 'error',
|
||||
// summary: t('settings.securityConnected.toast.telegramConnectFailedSummary'),
|
||||
// detail: t('settings.securityConnected.toast.telegramConnectFailedDetail'),
|
||||
// life: 5000
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
const disconnectTelegram = async () => {
|
||||
|
||||
@@ -75,7 +75,7 @@ import UserIcon from '@/components/icons/UserIcon.vue';
|
||||
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed } from 'vue';
|
||||
import { computed, createStaticVNode } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -98,7 +98,7 @@ const menuSections = computed<{ title: string; items: { value: string; label: st
|
||||
{
|
||||
title: t('settings.menu.securityGroup'),
|
||||
items: [
|
||||
{ value: 'security', label: t('settings.menu.security'), icon: UserIcon },
|
||||
{ value: 'security', label: t('settings.menu.security'), icon: createStaticVNode(`<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -258 596 564"><path d="M144-120c0-44 36-80 80-80s80 36 80 80-36 80-80 80-80-36-80-80zm208 0c0-71-57-128-128-128S96-191 96-120 153 8 224 8s128-57 128-128zM48 232c0-71 57-128 128-128h64V77c0-7 1-14 3-21h-67C79 56 0 135 0 232v8c0 13 11 24 24 24s24-11 24-24v-8zm397 9-13 6V59l96 32v19c0 56-32 107-83 131zM422 12 310 49c-13 4-22 16-22 30v31c0 75 43 142 110 174l19 9c5 2 10 3 15 3s10-1 15-3l19-9c67-32 110-99 110-174V79c0-14-9-26-22-30L442 11c-6-2-14-2-20 0zm0 0z" fill="currentColor"/></svg>`, 1) },
|
||||
{ value: 'billing', label: t('settings.menu.billing'), icon: CreditCardIcon },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ const errors = ref<{ title?: string }>({});
|
||||
const isFreePlan = computed(() => !auth.user?.plan_id);
|
||||
|
||||
const activeTemplates = computed(() =>
|
||||
adTemplates.value.filter(t => t.is_active),
|
||||
adTemplates.value.filter(t => t.isActive),
|
||||
);
|
||||
|
||||
const subtitleForm = ref({
|
||||
@@ -239,7 +239,7 @@ watch(() => props.videoId, (newId) => {
|
||||
:key="tmpl.id"
|
||||
:value="tmpl.id"
|
||||
>
|
||||
{{ tmpl.name }}{{ tmpl.is_default ? ` (${t('video.detailModal.adTemplateDefault')})` : '' }}
|
||||
{{ tmpl.name }}{{ tmpl.isDefault ? ` (${t('video.detailModal.adTemplateDefault')})` : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="isFreePlan" class="text-xs text-foreground/50 mt-0.5">
|
||||
|
||||
379
src/server/routes/rpc/admin.ts
Normal file
379
src/server/routes/rpc/admin.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import { validateFn } from "@hiogawa/tiny-rpc";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import z from "zod";
|
||||
|
||||
const optionalTrimmed = () => z.string().trim().min(1).optional();
|
||||
export const adminMethods = {
|
||||
getAdminDashboard: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
const response = await adminClient.getAdminDashboard({}, metadata);
|
||||
return response.dashboard ?? null;
|
||||
},
|
||||
listAdminUsers: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
search: optionalTrimmed(),
|
||||
role: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminUsers(data, metadata);
|
||||
}),
|
||||
createAdminUser: validateFn(
|
||||
z.object({
|
||||
email: z.string().trim().email(),
|
||||
username: optionalTrimmed(),
|
||||
password: z.string().min(6),
|
||||
role: z.string().trim().min(1),
|
||||
planId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminUser(data, metadata);
|
||||
}),
|
||||
updateAdminUser: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
email: z.string().trim().email().optional(),
|
||||
username: optionalTrimmed(),
|
||||
password: z.string().min(6).optional(),
|
||||
role: z.string().trim().min(1).optional(),
|
||||
planId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminUser(data, metadata);
|
||||
}),
|
||||
updateAdminUserRole: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
role: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminUserRole(data, metadata);
|
||||
}),
|
||||
deleteAdminUser: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminUser(data, metadata);
|
||||
}),
|
||||
listAdminVideos: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
search: optionalTrimmed(),
|
||||
userId: optionalTrimmed(),
|
||||
status: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminVideos(data, metadata);
|
||||
}),
|
||||
createAdminVideo: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
url: z.string().trim().url(),
|
||||
size: z.number().min(0).optional(),
|
||||
duration: z.number().min(0).optional(),
|
||||
format: optionalTrimmed(),
|
||||
status: z.string().trim().min(1),
|
||||
adTemplateId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminVideo(data, metadata);
|
||||
}),
|
||||
updateAdminVideo: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
url: z.string().trim().url(),
|
||||
size: z.number().min(0).optional(),
|
||||
duration: z.number().min(0).optional(),
|
||||
format: optionalTrimmed(),
|
||||
status: z.string().trim().min(1),
|
||||
adTemplateId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminVideo(data, metadata);
|
||||
}),
|
||||
deleteAdminVideo: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminVideo(data, metadata);
|
||||
}),
|
||||
listAdminPayments: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
userId: optionalTrimmed(),
|
||||
status: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminPayments(data, metadata);
|
||||
}),
|
||||
createAdminPayment: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
planId: z.string().trim().min(1),
|
||||
termMonths: z.number().int().min(1),
|
||||
paymentMethod: z.string().trim().min(1),
|
||||
topupAmount: z.number().min(0).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminPayment(data, metadata);
|
||||
}),
|
||||
updateAdminPayment: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
status: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminPayment(data, metadata);
|
||||
}),
|
||||
listAdminPlans: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminPlans({}, metadata);
|
||||
},
|
||||
createAdminPlan: validateFn(
|
||||
z.object({
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
features: z.array(z.string().trim().min(1)).optional(),
|
||||
price: z.number().min(0),
|
||||
cycle: z.string().trim().min(1),
|
||||
storageLimit: z.number().int().min(1),
|
||||
uploadLimit: z.number().int().min(1),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminPlan(data, metadata);
|
||||
}),
|
||||
updateAdminPlan: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
features: z.array(z.string().trim().min(1)).optional(),
|
||||
price: z.number().min(0),
|
||||
cycle: z.string().trim().min(1),
|
||||
storageLimit: z.number().int().min(1),
|
||||
uploadLimit: z.number().int().min(1),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminPlan(data, metadata);
|
||||
}),
|
||||
deleteAdminPlan: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminPlan(data, metadata);
|
||||
}),
|
||||
listAdminAdTemplates: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
userId: optionalTrimmed(),
|
||||
search: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminAdTemplates(data, metadata);
|
||||
}),
|
||||
createAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
vastTagUrl: z.string().trim().url(),
|
||||
adFormat: z.string().trim().min(1).optional(),
|
||||
duration: z.number().int().min(0).optional(),
|
||||
isActive: z.boolean(),
|
||||
isDefault: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
updateAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
vastTagUrl: z.string().trim().url(),
|
||||
adFormat: z.string().trim().min(1).optional(),
|
||||
duration: z.number().int().min(0).optional(),
|
||||
isActive: z.boolean(),
|
||||
isDefault: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
deleteAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
listAdminJobs: validateFn(
|
||||
z.object({
|
||||
offset: z.number().int().min(0).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
agentId: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminJobs(data, metadata);
|
||||
}),
|
||||
getAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.getAdminJob(data, metadata);
|
||||
}),
|
||||
getAdminJobLogs: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.getAdminJobLogs(data, metadata);
|
||||
}),
|
||||
createAdminJob: validateFn(
|
||||
z.object({
|
||||
command: z.string().trim().min(1),
|
||||
image: optionalTrimmed(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
priority: z.number().int().optional(),
|
||||
userId: optionalTrimmed(),
|
||||
name: optionalTrimmed(),
|
||||
timeLimit: z.number().int().min(0).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminJob(data, metadata);
|
||||
}),
|
||||
cancelAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.cancelAdminJob(data, metadata);
|
||||
}),
|
||||
retryAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.retryAdminJob(data, metadata);
|
||||
}),
|
||||
listAdminAgents: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminAgents({}, metadata);
|
||||
},
|
||||
restartAdminAgent: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.restartAdminAgent(data, metadata);
|
||||
}),
|
||||
updateAdminAgent: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminAgent(data, metadata);
|
||||
}),
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { authenticate } from "@/server/middlewares/authenticate";
|
||||
import { getGrpcMetadataFromContext } from "@/server/services/grpcClient";
|
||||
import { clientJSON, parse, stringify } from "@/shared/secure-json-transformer";
|
||||
import { parse, stringify } from "@/shared/secure-json-transformer";
|
||||
import { Metadata } from "@grpc/grpc-js";
|
||||
import { exposeTinyRpc, httpServerAdapter } from "@hiogawa/tiny-rpc";
|
||||
import { Hono } from "hono";
|
||||
import { protectedAuthMethods, publicAuthMethods } from "./auth";
|
||||
import { meMethods } from "./me";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import { adminMethods } from "./admin";
|
||||
|
||||
declare module "hono" {
|
||||
interface ContextVariableMap {
|
||||
@@ -18,6 +19,7 @@ const protectedRoutes = {
|
||||
health: () => ({ ok: true }),
|
||||
...protectedAuthMethods,
|
||||
...meMethods,
|
||||
...adminMethods
|
||||
};
|
||||
|
||||
const publicRoutes = {
|
||||
@@ -45,14 +47,16 @@ export function registerRpcRoutes(app: Hono) {
|
||||
};
|
||||
const protectedHandler = exposeTinyRpc({
|
||||
routes: protectedRoutes,
|
||||
adapter: httpServerAdapter({ endpoint: "/rpc", JSON: JSONProcessor }),
|
||||
adapter: httpServerAdapter({ endpoint: "/rpc",
|
||||
// JSON: JSONProcessor
|
||||
}),
|
||||
});
|
||||
app.use(publicEndpoint, async (c, next) => {
|
||||
const publicHandler = exposeTinyRpc({
|
||||
routes: publicRoutes,
|
||||
adapter: httpServerAdapter({
|
||||
endpoint: "/rpc-public",
|
||||
JSON: JSONProcessor,
|
||||
// JSON: JSONProcessor,
|
||||
}),
|
||||
});
|
||||
const res = await publicHandler({ request: c.req.raw });
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { validateFn } from "@hiogawa/tiny-rpc";
|
||||
// import { validateFn } from "@hiogawa/tiny-rpc";
|
||||
import { validateFn } from "@/server/utils";
|
||||
import { createManifest, saveManifest, validateChunkUrls } from "@/server/utils/s3Helper";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import z from "zod";
|
||||
|
||||
@@ -303,376 +305,43 @@ export const meMethods = {
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await paymentsClient.downloadInvoice(data, metadata);
|
||||
}),
|
||||
getAdminDashboard: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
const response = await adminClient.getAdminDashboard({}, metadata);
|
||||
return response.dashboard ?? null;
|
||||
},
|
||||
listAdminUsers: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
search: optionalTrimmed(),
|
||||
role: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminUsers(data, metadata);
|
||||
}),
|
||||
createAdminUser: validateFn(
|
||||
z.object({
|
||||
email: z.string().trim().email(),
|
||||
username: optionalTrimmed(),
|
||||
password: z.string().min(6),
|
||||
role: z.string().trim().min(1),
|
||||
planId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminUser(data, metadata);
|
||||
}),
|
||||
updateAdminUser: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
email: z.string().trim().email().optional(),
|
||||
username: optionalTrimmed(),
|
||||
password: z.string().min(6).optional(),
|
||||
role: z.string().trim().min(1).optional(),
|
||||
planId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminUser(data, metadata);
|
||||
}),
|
||||
updateAdminUserRole: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
role: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminUserRole(data, metadata);
|
||||
}),
|
||||
deleteAdminUser: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminUser(data, metadata);
|
||||
}),
|
||||
listAdminVideos: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
search: optionalTrimmed(),
|
||||
userId: optionalTrimmed(),
|
||||
status: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminVideos(data, metadata);
|
||||
}),
|
||||
createAdminVideo: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
url: z.string().trim().url(),
|
||||
size: z.number().min(0).optional(),
|
||||
duration: z.number().min(0).optional(),
|
||||
format: optionalTrimmed(),
|
||||
status: z.string().trim().min(1),
|
||||
adTemplateId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminVideo(data, metadata);
|
||||
}),
|
||||
updateAdminVideo: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
url: z.string().trim().url(),
|
||||
size: z.number().min(0).optional(),
|
||||
duration: z.number().min(0).optional(),
|
||||
format: optionalTrimmed(),
|
||||
status: z.string().trim().min(1),
|
||||
adTemplateId: optionalTrimmed(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminVideo(data, metadata);
|
||||
}),
|
||||
deleteAdminVideo: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminVideo(data, metadata);
|
||||
}),
|
||||
listAdminPayments: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
userId: optionalTrimmed(),
|
||||
status: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminPayments(data, metadata);
|
||||
}),
|
||||
createAdminPayment: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
planId: z.string().trim().min(1),
|
||||
termMonths: z.number().int().min(1),
|
||||
paymentMethod: z.string().trim().min(1),
|
||||
topupAmount: z.number().min(0).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminPayment(data, metadata);
|
||||
}),
|
||||
updateAdminPayment: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
status: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminPayment(data, metadata);
|
||||
}),
|
||||
listAdminPlans: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminPlans({}, metadata);
|
||||
},
|
||||
createAdminPlan: validateFn(
|
||||
z.object({
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
features: z.array(z.string().trim().min(1)).optional(),
|
||||
price: z.number().min(0),
|
||||
cycle: z.string().trim().min(1),
|
||||
storageLimit: z.number().int().min(1),
|
||||
uploadLimit: z.number().int().min(1),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminPlan(data, metadata);
|
||||
}),
|
||||
updateAdminPlan: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
features: z.array(z.string().trim().min(1)).optional(),
|
||||
price: z.number().min(0),
|
||||
cycle: z.string().trim().min(1),
|
||||
storageLimit: z.number().int().min(1),
|
||||
uploadLimit: z.number().int().min(1),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminPlan(data, metadata);
|
||||
}),
|
||||
deleteAdminPlan: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminPlan(data, metadata);
|
||||
}),
|
||||
listAdminAdTemplates: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
userId: optionalTrimmed(),
|
||||
search: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminAdTemplates(data, metadata);
|
||||
}),
|
||||
createAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
vastTagUrl: z.string().trim().url(),
|
||||
adFormat: z.string().trim().min(1).optional(),
|
||||
duration: z.number().int().min(0).optional(),
|
||||
isActive: z.boolean(),
|
||||
isDefault: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
updateAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
name: z.string().trim().min(1),
|
||||
description: optionalTrimmed(),
|
||||
vastTagUrl: z.string().trim().url(),
|
||||
adFormat: z.string().trim().min(1).optional(),
|
||||
duration: z.number().int().min(0).optional(),
|
||||
isActive: z.boolean(),
|
||||
isDefault: z.boolean(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
deleteAdminAdTemplate: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
listAdminJobs: validateFn(
|
||||
z.object({
|
||||
offset: z.number().int().min(0).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
agentId: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminJobs(data, metadata);
|
||||
}),
|
||||
getAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.getAdminJob(data, metadata);
|
||||
}),
|
||||
getAdminJobLogs: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.getAdminJobLogs(data, metadata);
|
||||
}),
|
||||
createAdminJob: validateFn(
|
||||
z.object({
|
||||
command: z.string().trim().min(1),
|
||||
image: optionalTrimmed(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
priority: z.number().int().optional(),
|
||||
userId: optionalTrimmed(),
|
||||
name: optionalTrimmed(),
|
||||
timeLimit: z.number().int().min(0).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminJob(data, metadata);
|
||||
}),
|
||||
cancelAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.cancelAdminJob(data, metadata);
|
||||
}),
|
||||
retryAdminJob: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.retryAdminJob(data, metadata);
|
||||
}),
|
||||
listAdminAgents: async () => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminAgents({}, metadata);
|
||||
},
|
||||
restartAdminAgent: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.restartAdminAgent(data, metadata);
|
||||
}),
|
||||
updateAdminAgent: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminServiceClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminAgent(data, metadata);
|
||||
merge: validateFn(
|
||||
z.string().trim().min(6).includes("."),
|
||||
z.array(z.url()).min(1).refine(validateChunkUrls, { message: "One or more chunk URLs are invalid or not allowed" }),
|
||||
z.number().min(5 * 1024 * 1024),// min 5mb
|
||||
)
|
||||
(async (filename, chunks, size) => {
|
||||
const manifest = createManifest(filename, chunks, size);
|
||||
await saveManifest(manifest);
|
||||
return manifest;
|
||||
// return await videosClient.merge({ name, chunks, size }, metadata);
|
||||
}),
|
||||
};
|
||||
// export function registerMergeRoutes(app: Hono) {
|
||||
// app.post('/merge', authMiddleware, async (c) => {
|
||||
// try {
|
||||
// const body = await c.req.json();
|
||||
// const { filename, chunks, size } = body;
|
||||
|
||||
// if (!filename || !Array.isArray(chunks) || chunks.length === 0) {
|
||||
// return c.json({ error: 'invalid payload' }, 400);
|
||||
// }
|
||||
|
||||
// const hostError = validateChunkUrls(chunks);
|
||||
// if (hostError) return c.json({ error: hostError }, 400);
|
||||
|
||||
// const manifest = createManifest(filename, chunks, size);
|
||||
// await saveManifest(manifest);
|
||||
|
||||
// return c.json({
|
||||
// status: 'ok',
|
||||
// id: manifest.id,
|
||||
// filename: manifest.filename,
|
||||
// total_parts: manifest.total_parts,
|
||||
// size: manifest.size,
|
||||
// });
|
||||
// } catch (e: any) {
|
||||
// return c.json({ error: e?.message ?? String(e) }, 500);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { User } from "@/server/gen/proto/app/v1/common";
|
||||
import { TinyRpcError } from "@hiogawa/tiny-rpc";
|
||||
import { tinyassert } from "@hiogawa/utils";
|
||||
import { Context } from "hono";
|
||||
import { tryGetContext } from "hono/context-storage";
|
||||
import { setCookie } from "hono/cookie";
|
||||
@@ -58,4 +60,65 @@ export const class2Object = <T>(classConvert: T) => {
|
||||
return classAsObj
|
||||
}, {})
|
||||
return object as T
|
||||
}
|
||||
// validator agnostic function guard
|
||||
// now supports multiple schemas / multiple args
|
||||
// (it only supports zod for starter)
|
||||
|
||||
export function validateFn<Schemas extends readonly unknown[]>(
|
||||
...schemas: Schemas
|
||||
) {
|
||||
return function decorate<Out>(
|
||||
fn: (...inputs: InferOutputs<Schemas>) => Out
|
||||
): (...inputRaws: InferInputs<Schemas>) => Out {
|
||||
return function wrapper(...inputRaws) {
|
||||
if (inputRaws.length !== schemas.length) {
|
||||
throw new TinyRpcError(
|
||||
`invalid argument count: expected ${schemas.length}, got ${inputRaws.length}`
|
||||
).setStatus(400);
|
||||
}
|
||||
|
||||
const inputs = schemas.map((schema, index) => {
|
||||
const parser = getParser(schema);
|
||||
try {
|
||||
return parser(inputRaws[index]);
|
||||
} catch (e) {
|
||||
throw TinyRpcError.fromUnknown(`Error validating argument at index ${index}: ${e instanceof Error ? e.message : 'Unknown error'}`).setStatus(400);
|
||||
}
|
||||
}) as InferOutputs<Schemas>;
|
||||
|
||||
return fn(...inputs);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// infer input/output from a single parser
|
||||
type InferIO<Parser> = Parser extends { _input: infer I; _output: infer O }
|
||||
? {
|
||||
i: I;
|
||||
o: O;
|
||||
}
|
||||
: {
|
||||
i: never;
|
||||
o: never;
|
||||
};
|
||||
|
||||
// infer tuple of raw inputs from tuple of schemas
|
||||
type InferInputs<Schemas extends readonly unknown[]> = {
|
||||
[K in keyof Schemas]: InferIO<Schemas[K]>["i"];
|
||||
};
|
||||
|
||||
// infer tuple of parsed outputs from tuple of schemas
|
||||
type InferOutputs<Schemas extends readonly unknown[]> = {
|
||||
[K in keyof Schemas]: InferIO<Schemas[K]>["o"];
|
||||
};
|
||||
|
||||
function getParser(schema: unknown): (input: unknown) => unknown {
|
||||
tinyassert(schema && typeof schema === "object");
|
||||
|
||||
if ("parse" in schema && typeof schema.parse === "function") {
|
||||
return schema.parse.bind(schema);
|
||||
}
|
||||
|
||||
throw new TinyRpcError("unsupported schema", { cause: schema });
|
||||
}
|
||||
232
src/server/utils/s3Helper.ts
Normal file
232
src/server/utils/s3Helper.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
import { AwsClient } from 'aws4fetch';
|
||||
|
||||
export type Part = {
|
||||
index: number
|
||||
host: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type Manifest = {
|
||||
version: 1
|
||||
id: string
|
||||
filename: string
|
||||
total_parts: number
|
||||
parts: Part[]
|
||||
createdAt: number
|
||||
expiresAt: number
|
||||
size: number
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// S3 Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const S3_ENDPOINT = "https://minio1.webtui.vn:9000"
|
||||
const BUCKET_NAME = "bucket-lethdat"
|
||||
|
||||
const aws = new AwsClient({
|
||||
accessKeyId: "lethdat",
|
||||
secretAccessKey: "D@tkhong9",
|
||||
service: 's3',
|
||||
region: 'auto'
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// S3 Operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const OBJECT_KEY = (id: string) => `${id}.json`
|
||||
|
||||
/** Persist a manifest as JSON in MinIO. */
|
||||
export async function saveManifest(manifest: Manifest): Promise<void> {
|
||||
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(manifest.id)}`;
|
||||
|
||||
const response = await aws.fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(manifest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save manifest: ${response.status} ${await response.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch a manifest from MinIO. */
|
||||
export async function getManifest(id: string): Promise<Manifest | null> {
|
||||
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(id)}`;
|
||||
|
||||
try {
|
||||
const response = await aws.fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get manifest: ${response.status}`)
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
const manifest: Manifest = JSON.parse(text)
|
||||
|
||||
if (manifest.expiresAt < Date.now()) {
|
||||
await deleteManifest(id).catch(() => { })
|
||||
return null
|
||||
}
|
||||
|
||||
return manifest
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a manifest object from MinIO. */
|
||||
export async function deleteManifest(id: string): Promise<void> {
|
||||
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(id)}`;
|
||||
|
||||
const response = await aws.fetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error(`Failed to delete manifest: ${response.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Allowed chunk source hosts
|
||||
const ALLOWED_HOSTS = [
|
||||
'tmpfiles.org',
|
||||
'gofile.io',
|
||||
'pixeldrain.com',
|
||||
'uploadfiles.io',
|
||||
'anonfiles.com',
|
||||
]
|
||||
|
||||
/** Returns an error message if any URL is disallowed, otherwise null. */
|
||||
export function validateChunkUrls(chunks: string[]): boolean {
|
||||
for (const u of chunks) {
|
||||
try {
|
||||
const { hostname } = new URL(u)
|
||||
if (!ALLOWED_HOSTS.some(h => hostname.includes(h))) {
|
||||
return false
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function sanitizeFilename(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
}
|
||||
|
||||
export function detectHost(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '')
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function formatUrl(url: string): string {
|
||||
if (url.includes("tmpfiles.org/") && !url.includes("tmpfiles.org/dl/")) {
|
||||
return url.trim().replace("tmpfiles.org/", 'tmpfiles.org/dl/')
|
||||
}
|
||||
return url.trim()
|
||||
}
|
||||
|
||||
/** List all manifests in bucket (simple implementation). */
|
||||
export async function getListFiles(): Promise<string[]> {
|
||||
// For now return empty array - implement listing if needed
|
||||
// MinIO S3 ListObjectsV2 would require XML parsing
|
||||
return []
|
||||
}
|
||||
|
||||
/** Build a new Manifest. */
|
||||
export function createManifest(
|
||||
filename: string,
|
||||
chunks: string[],
|
||||
size: number,
|
||||
ttlMs = 60 * 60 * 1000,
|
||||
): Manifest {
|
||||
const id = crypto.randomUUID()
|
||||
const now = Date.now()
|
||||
return {
|
||||
version: 1,
|
||||
id,
|
||||
filename: sanitizeFilename(filename),
|
||||
total_parts: chunks.length,
|
||||
parts: chunks.map((url, index) => ({ index, host: detectHost(url), url: formatUrl(url) })),
|
||||
createdAt: now,
|
||||
expiresAt: now + ttlMs,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Streaming
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Streams all parts in index order as one continuous ReadableStream. */
|
||||
export function streamManifest(manifest: Manifest): ReadableStream<Uint8Array> {
|
||||
const parts = [...manifest.parts].sort((a, b) => a.index - b.index)
|
||||
const RETRY = 3
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
for (const part of parts) {
|
||||
let attempt = 0
|
||||
let ok = false
|
||||
while (attempt < RETRY && !ok) {
|
||||
attempt++
|
||||
try {
|
||||
const res = await fetch(formatUrl(part.url))
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const reader = res.body!.getReader()
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
controller.enqueue(value)
|
||||
}
|
||||
ok = true
|
||||
} catch (err: any) {
|
||||
if (attempt >= RETRY) {
|
||||
controller.error(new Error(`Part ${part.index} failed: ${err?.message ?? err}`))
|
||||
return
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000 * attempt))
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
export async function saveImageFromStream(stream: ArrayBuffer, filename: string): Promise<void> {
|
||||
// Implement this function to save the thumbnail image stream to storage and update the database with the thumbnail URL
|
||||
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${filename}.jpg`;
|
||||
|
||||
const response = await aws.fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
},
|
||||
body: stream,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save thumbnail: ${response.status} ${await response.text()}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user