mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
feat: Configurator page finished
This commit is contained in:
@@ -1,14 +1,32 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { type AsyncZippable, strToU8, zipSync } from "fflate";
|
||||||
|
import { type z } from "zod";
|
||||||
import { type ActionResponseSchema } from "~/types";
|
import { type ActionResponseSchema } from "~/types";
|
||||||
|
|
||||||
import { base64Decode, base64Encode } from "~/lib/utils.server";
|
import {
|
||||||
|
configurationTemplate,
|
||||||
|
environmentTemplate,
|
||||||
|
parseEnvironment,
|
||||||
|
parseVersion1Config,
|
||||||
|
parseVersion2Config,
|
||||||
|
} from "~/lib/configurationHelper";
|
||||||
|
import { base64Decode, base64Encode, encryptionService } from "~/lib/utils.server";
|
||||||
|
|
||||||
|
import { type Schema_App_Configuration, type Schema_App_Configuration_Env } from "~/types/schema";
|
||||||
|
|
||||||
export async function GenerateServiceAccountB64(serviceAccount: string): Promise<ActionResponseSchema<string>> {
|
export async function GenerateServiceAccountB64(serviceAccount: string): Promise<ActionResponseSchema<string>> {
|
||||||
const b64 = base64Encode(serviceAccount, "standard");
|
const b64 = base64Encode(serviceAccount, "standard");
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
const decoded = base64Decode(b64, "standard");
|
const decoded = base64Decode(b64, "standard");
|
||||||
|
if (decoded === null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to decode the service account",
|
||||||
|
error: "Something went wrong while encoding the service account, please try again",
|
||||||
|
};
|
||||||
|
}
|
||||||
if (decoded !== serviceAccount) {
|
if (decoded !== serviceAccount) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -23,3 +41,241 @@ export async function GenerateServiceAccountB64(serviceAccount: string): Promise
|
|||||||
data: b64,
|
data: b64,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ProcessEnvironmentConfig(
|
||||||
|
configuration: string,
|
||||||
|
): Promise<ActionResponseSchema<z.infer<typeof Schema_App_Configuration_Env>>> {
|
||||||
|
const data = parseEnvironment(configuration);
|
||||||
|
if ("message" in data && "details" in data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: data.message,
|
||||||
|
error: data.details.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Verify service account
|
||||||
|
const serviceAccount = data.GD_SERVICE_B64;
|
||||||
|
const decoded = base64Decode(serviceAccount, "standard");
|
||||||
|
if (decoded === null) {
|
||||||
|
data.GD_SERVICE_B64 = "";
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
message: "Environment loaded, but service account is invalid",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Environment loaded",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ProcessConfiguration(
|
||||||
|
configuration: string,
|
||||||
|
version: "v1" | "v2" | "latest",
|
||||||
|
): Promise<ActionResponseSchema<Omit<z.infer<typeof Schema_App_Configuration>, "environment">>> {
|
||||||
|
const data = version === "v1" ? parseVersion1Config(configuration) : parseVersion2Config(configuration);
|
||||||
|
if ("message" in data && "details" in data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: data.message,
|
||||||
|
error: data.details.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Configuration loaded, but folder ID are not processed",
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GenerateConfiguration(
|
||||||
|
values: z.infer<typeof Schema_App_Configuration>,
|
||||||
|
): Promise<ActionResponseSchema<{ configuration: string; env: string; zip: Blob }>> {
|
||||||
|
const configurationMap: { key: string; value: string }[] = [
|
||||||
|
{
|
||||||
|
key: "version",
|
||||||
|
value: values.version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "showGuideButton",
|
||||||
|
value: values.site.guideButton.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cacheControl",
|
||||||
|
value: `${values.api.cache.public ? "public, " : ""}max-age=${values.api.cache.maxAge}, s-maxage=${
|
||||||
|
values.api.cache.sMaxAge
|
||||||
|
}${values.api.cache.staleWhileRevalidate ? ", stale-while-revalidate" : ""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.rootFolder",
|
||||||
|
value: await encryptionService.encrypt(values.api.rootFolder, values.environment.ENCRYPTION_KEY),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.isTeamDrive",
|
||||||
|
value: values.api.isTeamDrive.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.sharedDrive",
|
||||||
|
value: values.api.sharedDrive
|
||||||
|
? await encryptionService.encrypt(values.api.sharedDrive, values.environment.ENCRYPTION_KEY)
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.itemsPerPage",
|
||||||
|
value: values.api.itemsPerPage.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.searchResult",
|
||||||
|
value: values.api.searchResult.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.specialFile.password",
|
||||||
|
value: values.api.specialFile.password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.specialFile.readme",
|
||||||
|
value: values.api.specialFile.readme,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.specialFile.banner",
|
||||||
|
value: values.api.specialFile.banner,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.hiddenFiles",
|
||||||
|
value: `[${values.api.hiddenFiles.map((file) => `"${file}"`).join(", ")}]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.proxyThumbnail",
|
||||||
|
value: values.api.proxyThumbnail.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.streamMaxSize",
|
||||||
|
value: values.api.streamMaxSize.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.maxFileSize",
|
||||||
|
value: values.api.maxFileSize.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.allowDownloadProtectedFile",
|
||||||
|
value: values.api.allowDownloadProtectedFile.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api.temporaryTokenDuration",
|
||||||
|
value: values.api.temporaryTokenDuration.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.siteName",
|
||||||
|
value: values.site.siteName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.siteNameTemplate",
|
||||||
|
value: values.site.siteNameTemplate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.siteDescription",
|
||||||
|
value: values.site.siteDescription,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.siteAuthor",
|
||||||
|
value: values.site.siteAuthor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.robots",
|
||||||
|
value: values.site.robots,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.twitterHandle",
|
||||||
|
value: values.site.twitterHandle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.showFileExtension",
|
||||||
|
value: values.site.showFileExtension.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.privateIndex",
|
||||||
|
value: values.site.privateIndex.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.breadcrumbMax",
|
||||||
|
value: values.site.breadcrumbMax.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.toaster.position",
|
||||||
|
value: values.site.toaster.position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.toaster.duration",
|
||||||
|
value: values.site.toaster.duration.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.navbarItems",
|
||||||
|
value: JSON.stringify(values.site.navbarItems, null, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.supports",
|
||||||
|
value: JSON.stringify(values.site.supports, null, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "site.footer",
|
||||||
|
value: JSON.stringify(values.site.footer, null, 2),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let configuration = configurationTemplate;
|
||||||
|
for (const { key, value } of configurationMap) {
|
||||||
|
configuration = configuration.replace(`{{ ${key} }}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envMap: { key: string; value: string }[] = [
|
||||||
|
{
|
||||||
|
key: "serviceAccount",
|
||||||
|
value: values.environment.GD_SERVICE_B64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "key",
|
||||||
|
value: values.environment.ENCRYPTION_KEY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "password",
|
||||||
|
value: values.environment.SITE_PASSWORD ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "domain",
|
||||||
|
value: values.environment.NEXT_PUBLIC_DOMAIN ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let env = environmentTemplate;
|
||||||
|
for (const { key, value } of envMap) {
|
||||||
|
env = env.replace(`{{ ${key} }}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct: AsyncZippable = {
|
||||||
|
".env": [
|
||||||
|
strToU8(env),
|
||||||
|
{
|
||||||
|
level: 9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"gIndex.config.ts": [
|
||||||
|
strToU8(configuration),
|
||||||
|
{
|
||||||
|
level: 9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const zip = zipSync(struct);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Configuration generated",
|
||||||
|
data: {
|
||||||
|
configuration,
|
||||||
|
env,
|
||||||
|
zip: new Blob([zip], { type: "application/zip" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Switch } from "~/components/ui/switch";
|
|||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
import { FormColumn, FormProps, FormSection } from "./ConfiguratorPage";
|
import { FormColumn, type FormProps, FormSection } from "./ConfiguratorPage";
|
||||||
|
|
||||||
export default function ApiForm({ form, onResetField }: FormProps) {
|
export default function ApiForm({ form, onResetField }: FormProps) {
|
||||||
const [inputHiddenFile, setInputHiddenFile] = useState<string>("");
|
const [inputHiddenFile, setInputHiddenFile] = useState<string>("");
|
||||||
@@ -181,7 +181,7 @@ export default function ApiForm({ form, onResetField }: FormProps) {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='api.sharedDrive'
|
name='api.sharedDrive'
|
||||||
render={({ field, fieldState, formState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className='inline-flex w-full items-center justify-between gap-4 tablet:justify-start'>
|
<div className='inline-flex w-full items-center justify-between gap-4 tablet:justify-start'>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
@@ -341,7 +341,7 @@ export default function ApiForm({ form, onResetField }: FormProps) {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='api.hiddenFiles'
|
name='api.hiddenFiles'
|
||||||
render={({ field, fieldState }) => {
|
render={({ fieldState }) => {
|
||||||
const watch = form.watch("api.hiddenFiles");
|
const watch = form.watch("api.hiddenFiles");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useFieldArray } from "react-hook-form";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkBreaks from "remark-breaks";
|
import remarkBreaks from "remark-breaks";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { type z } from "zod";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { VirtualizedCombobox } from "~/components/ui/combobox.virtualized";
|
import { VirtualizedCombobox } from "~/components/ui/combobox.virtualized";
|
||||||
@@ -31,9 +31,9 @@ import { Textarea } from "~/components/ui/textarea";
|
|||||||
import { useResponsive } from "~/context/responsiveContext";
|
import { useResponsive } from "~/context/responsiveContext";
|
||||||
import { cn, formatFooterContent } from "~/lib/utils";
|
import { cn, formatFooterContent } from "~/lib/utils";
|
||||||
|
|
||||||
import { Schema_App_Configuration } from "~/types/schema";
|
import { type Schema_App_Configuration } from "~/types/schema";
|
||||||
|
|
||||||
import { FormColumn, FormProps, FormSection } from "./ConfiguratorPage";
|
import { FormColumn, type FormProps, FormSection } from "./ConfiguratorPage";
|
||||||
|
|
||||||
export default function SiteForm({ form, onResetField }: FormProps) {
|
export default function SiteForm({ form, onResetField }: FormProps) {
|
||||||
return (
|
return (
|
||||||
@@ -297,7 +297,7 @@ export default function SiteForm({ form, onResetField }: FormProps) {
|
|||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Maximum number of breadcrumbs item before it's truncated.</FormDescription>
|
<FormDescription>Maximum number of breadcrumbs item before it's truncated.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -366,6 +366,7 @@ export default function SiteForm({ form, onResetField }: FormProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
className='col-span-full w-full'
|
className='col-span-full w-full'
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -426,6 +427,7 @@ function NavbarItemsField({ form, onResetField }: FormProps) {
|
|||||||
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
||||||
<Label>Navbar Items</Label>
|
<Label>Navbar Items</Label>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant={"ghost"}
|
variant={"ghost"}
|
||||||
disabled={!form.getFieldState("site.navbarItems").isDirty}
|
disabled={!form.getFieldState("site.navbarItems").isDirty}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -493,7 +495,7 @@ function NavbarItemsField({ form, onResetField }: FormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='inline-flex w-full flex-col gap-4 tablet:flex-row '>
|
<div className='inline-flex w-full flex-col gap-4 tablet:flex-row tablet:items-end'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`site.navbarItems.${index}.href`}
|
name={`site.navbarItems.${index}.href`}
|
||||||
@@ -518,7 +520,6 @@ function NavbarItemsField({ form, onResetField }: FormProps) {
|
|||||||
name={`site.navbarItems.${index}.external`}
|
name={`site.navbarItems.${index}.external`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem disableBorder>
|
<FormItem disableBorder>
|
||||||
<FormLabel>External Link</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -534,7 +535,7 @@ function NavbarItemsField({ form, onResetField }: FormProps) {
|
|||||||
field.value ? "opacity-100" : "opacity-30",
|
field.value ? "opacity-100" : "opacity-30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Open in new tab
|
External Link
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -558,8 +559,8 @@ function NavbarItemsField({ form, onResetField }: FormProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className='w-full'
|
|
||||||
type='button'
|
type='button'
|
||||||
|
className='w-full'
|
||||||
onClick={() => append({ icon: "Link", name: "New Item", href: "/new-item", external: false })}
|
onClick={() => append({ icon: "Link", name: "New Item", href: "/new-item", external: false })}
|
||||||
>
|
>
|
||||||
<Icon name='Plus' />
|
<Icon name='Plus' />
|
||||||
@@ -583,6 +584,7 @@ function SupportsField({ form, onResetField }: FormProps) {
|
|||||||
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
||||||
<Label>Supports / Donations</Label>
|
<Label>Supports / Donations</Label>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant={"ghost"}
|
variant={"ghost"}
|
||||||
disabled={!form.getFieldState("site.supports").isDirty}
|
disabled={!form.getFieldState("site.supports").isDirty}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -746,6 +748,7 @@ function FooterField({ form, onResetField }: FormProps) {
|
|||||||
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
<div className='flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start'>
|
||||||
<Label>Footer Items</Label>
|
<Label>Footer Items</Label>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant={"ghost"}
|
variant={"ghost"}
|
||||||
disabled={!form.getFieldState("site.footer").isDirty}
|
disabled={!form.getFieldState("site.footer").isDirty}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEvent, useRef, useState } from "react";
|
import { type ChangeEvent, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { LoadingButton } from "~/components/ui/button";
|
import { LoadingButton } from "~/components/ui/button";
|
||||||
@@ -7,7 +7,7 @@ import { Input } from "~/components/ui/input";
|
|||||||
|
|
||||||
import { GenerateServiceAccountB64 } from "~/actions/configuration";
|
import { GenerateServiceAccountB64 } from "~/actions/configuration";
|
||||||
|
|
||||||
import { FormProps, FormSection } from "./ConfiguratorPage";
|
import { type FormProps, FormSection } from "./ConfiguratorPage";
|
||||||
|
|
||||||
export default function EnvironmentForm({ onResetField, form }: FormProps) {
|
export default function EnvironmentForm({ onResetField, form }: FormProps) {
|
||||||
const serviceInputRef = useRef<HTMLInputElement>(null);
|
const serviceInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -86,6 +86,7 @@ export default function EnvironmentForm({ onResetField, form }: FormProps) {
|
|||||||
loading={serviceInputLoading}
|
loading={serviceInputLoading}
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
onClick={() => serviceInputRef.current?.click()}
|
onClick={() => serviceInputRef.current?.click()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Load JSON
|
Load JSON
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|||||||
@@ -3,105 +3,29 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { type PropsWithChildren, useState } from "react";
|
import { type PropsWithChildren, useState } from "react";
|
||||||
import { FieldPath, type UseFormReturn, useForm } from "react-hook-form";
|
import { type FieldPath, type UseFormReturn, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type z } from "zod";
|
import { type z } from "zod";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button, LoadingButton } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import {
|
|
||||||
ResponsiveDropdownMenu,
|
|
||||||
ResponsiveDropdownMenuContent,
|
|
||||||
ResponsiveDropdownMenuItem,
|
|
||||||
ResponsiveDropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu.responsive";
|
|
||||||
import { Form } from "~/components/ui/form";
|
import { Form } from "~/components/ui/form";
|
||||||
import Icon from "~/components/ui/icon";
|
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
|
||||||
import { useResponsive } from "~/context/responsiveContext";
|
import { type PickFileResponse, initialConfiguration, pickFile, versionExpectMap } from "~/lib/configurationHelper";
|
||||||
|
|
||||||
import { type ConfigurationCategory, Schema_App_Configuration } from "~/types/schema";
|
import { type ConfigurationCategory, Schema_App_Configuration } from "~/types/schema";
|
||||||
|
|
||||||
|
import { GenerateConfiguration, ProcessConfiguration, ProcessEnvironmentConfig } from "~/actions/configuration";
|
||||||
|
|
||||||
import ApiForm from "./ConfigurationPage.Api";
|
import ApiForm from "./ConfigurationPage.Api";
|
||||||
import SiteForm from "./ConfigurationPage.Site";
|
import SiteForm from "./ConfigurationPage.Site";
|
||||||
import EnvironmentForm from "./ConfiguratorPage.Environment";
|
import EnvironmentForm from "./ConfiguratorPage.Environment";
|
||||||
|
|
||||||
const initialConfiguration: z.input<typeof Schema_App_Configuration> = {
|
|
||||||
environment: {
|
|
||||||
ENCRYPTION_KEY: "",
|
|
||||||
SITE_PASSWORD: "",
|
|
||||||
GD_SERVICE_B64: "",
|
|
||||||
NEXT_PUBLIC_DOMAIN: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
api: {
|
|
||||||
cache: {
|
|
||||||
public: true,
|
|
||||||
maxAge: 60,
|
|
||||||
sMaxAge: 60,
|
|
||||||
staleWhileRevalidate: true,
|
|
||||||
},
|
|
||||||
rootFolder: "",
|
|
||||||
isTeamDrive: false,
|
|
||||||
sharedDrive: "",
|
|
||||||
defaultQuery: ["trashed = false", "(not mimeType contains 'google-apps' or mimeType contains 'folder')"],
|
|
||||||
defaultField:
|
|
||||||
"id, name, mimeType, thumbnailLink, fileExtension, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, trashed",
|
|
||||||
defaultOrder: "folder, name asc, modifiedTime desc",
|
|
||||||
itemsPerPage: 50,
|
|
||||||
searchResult: 5,
|
|
||||||
proxyThumbnail: true,
|
|
||||||
streamMaxSize: 100 * 1024 * 1024,
|
|
||||||
specialFile: {
|
|
||||||
password: ".password",
|
|
||||||
readme: ".readme.md",
|
|
||||||
banner: ".banner",
|
|
||||||
},
|
|
||||||
hiddenFiles: [".password", ".readme.md", ".banner", ".banner.jpg", ".banner.png", ".banner.webp"],
|
|
||||||
allowDownloadProtectedFile: false,
|
|
||||||
temporaryTokenDuration: 6,
|
|
||||||
maxFileSize: 4 * 1024 * 1024,
|
|
||||||
},
|
|
||||||
|
|
||||||
site: {
|
|
||||||
guideButton: false,
|
|
||||||
|
|
||||||
siteName: "next-gdrive-index",
|
|
||||||
siteNameTemplate: "%s - %t",
|
|
||||||
siteDescription: "A simple file browser for Google Drive",
|
|
||||||
siteIcon: "/logo.svg",
|
|
||||||
siteAuthor: "mbaharip",
|
|
||||||
favIcon: "/favicon.png",
|
|
||||||
|
|
||||||
robots: "noindex, nofollow",
|
|
||||||
twitterHandle: "@mbaharip_",
|
|
||||||
showFileExtension: true,
|
|
||||||
footer: [
|
|
||||||
{ value: "{{ poweredBy }}" },
|
|
||||||
{ value: "Made with ❤️ by [**{{ author }}**](https://github.com/mbaharip)" },
|
|
||||||
],
|
|
||||||
experimental_pageLoadTime: false,
|
|
||||||
privateIndex: false,
|
|
||||||
breadcrumbMax: 3,
|
|
||||||
toaster: {
|
|
||||||
position: "bottom-right",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
navbarItems: [],
|
|
||||||
supports: [],
|
|
||||||
previewSettings: {
|
|
||||||
manga: {
|
|
||||||
maxSize: 15 * 1024 * 1024,
|
|
||||||
maxItem: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ConfiguratorPage() {
|
export default function ConfiguratorPage() {
|
||||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
const [isLoadingEnv, setIsLoadingEnv] = useState<boolean>(false);
|
||||||
|
const [isLoadingConfig, setIsLoadingConfig] = useState<boolean>(false);
|
||||||
const form = useForm<z.infer<typeof Schema_App_Configuration>>({
|
const form = useForm<z.infer<typeof Schema_App_Configuration>>({
|
||||||
resolver: zodResolver(Schema_App_Configuration),
|
resolver: zodResolver(Schema_App_Configuration),
|
||||||
defaultValues: initialConfiguration,
|
defaultValues: initialConfiguration,
|
||||||
@@ -113,11 +37,146 @@ export default function ConfiguratorPage() {
|
|||||||
} else {
|
} else {
|
||||||
form.resetField(category);
|
form.resetField(category);
|
||||||
}
|
}
|
||||||
|
toast.success("Form reverted to initial state");
|
||||||
}
|
}
|
||||||
function onFormSubmit(values: z.infer<typeof Schema_App_Configuration>) {
|
async function onFormSubmit(values: z.infer<typeof Schema_App_Configuration>) {
|
||||||
toast.info("Form submitted", {
|
const id = `download-${Date.now()}`;
|
||||||
description: <pre className='w-full overflow-auto'>{JSON.stringify(values, null, 2)}</pre>,
|
toast.loading("Generating configuration...", {
|
||||||
|
id,
|
||||||
|
duration: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = await GenerateConfiguration(values);
|
||||||
|
if (!data.success) {
|
||||||
|
toast.error(data.message, {
|
||||||
|
id,
|
||||||
|
description: data.error,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(data.data.zip);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${Date.now()}-gIndex.config.zip`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
a.remove();
|
||||||
|
|
||||||
|
toast.success("Configuration generated", {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLoadEnv(response: PickFileResponse) {
|
||||||
|
const id = `env-latest-${Date.now()}`;
|
||||||
|
toast.loading("Waiting for file...", {
|
||||||
|
id,
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
if (!response.success) {
|
||||||
|
toast.error(response.message, {
|
||||||
|
id,
|
||||||
|
description: response.details.length ? (
|
||||||
|
<pre className='w-full overflow-auto whitespace-pre-wrap font-mono text-xs'>
|
||||||
|
{response.details.join("\n")}
|
||||||
|
</pre>
|
||||||
|
) : undefined,
|
||||||
|
});
|
||||||
|
setIsLoadingEnv(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.loading("Processing environment file...", {
|
||||||
|
id,
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
const data = await ProcessEnvironmentConfig(response.data);
|
||||||
|
if (!data.success) {
|
||||||
|
toast.error(data.message, {
|
||||||
|
id,
|
||||||
|
description: <pre className='w-full overflow-auto whitespace-pre-wrap font-mono text-xs'>{data.error}</pre>,
|
||||||
|
});
|
||||||
|
setIsLoadingEnv(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setValue("environment", data.data);
|
||||||
|
|
||||||
|
toast.success(data.message, {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
setIsLoadingEnv(false);
|
||||||
|
}
|
||||||
|
async function onLoadConfig(response: PickFileResponse) {
|
||||||
|
const id = `config-${Date.now()}`;
|
||||||
|
toast.loading("Waiting for file...", {
|
||||||
|
id,
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
if (!response.success) {
|
||||||
|
toast.error(response.message, {
|
||||||
|
id,
|
||||||
|
description: response.details.length ? (
|
||||||
|
<pre className='w-full overflow-auto whitespace-pre-wrap font-mono text-xs'>
|
||||||
|
{response.details.join("\n")}
|
||||||
|
</pre>
|
||||||
|
) : undefined,
|
||||||
|
});
|
||||||
|
setIsLoadingConfig(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedVersion = /version:\s*["']?(\d+\.\d+\.\d+)["']?/.exec(response.data)?.[1];
|
||||||
|
if (!loadedVersion) {
|
||||||
|
toast.error("Version not found in configuration file", {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
setIsLoadingConfig(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionGroup = Object.entries(versionExpectMap).find(([_, v]) => v.includes(loadedVersion))?.[0];
|
||||||
|
if (!versionGroup) {
|
||||||
|
toast.error("Version not recognized", {
|
||||||
|
id,
|
||||||
|
description: (
|
||||||
|
<pre className='w-full overflow-auto whitespace-pre-wrap font-mono text-xs'>
|
||||||
|
{`Loaded version: ${loadedVersion}, not matching any known version`}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
setIsLoadingConfig(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.loading(`Version ${loadedVersion} detected, processing...`, {
|
||||||
|
id,
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
const data = await ProcessConfiguration(response.data, versionGroup as "v1" | "v2" | "latest");
|
||||||
|
if (!data.success) {
|
||||||
|
toast.error(data.message, {
|
||||||
|
id,
|
||||||
|
description: <pre className='w-full overflow-auto whitespace-pre-wrap font-mono text-xs'>{data.error}</pre>,
|
||||||
|
});
|
||||||
|
setIsLoadingConfig(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setValue("api", data.data.api);
|
||||||
|
form.setValue("site", data.data.site);
|
||||||
|
form.setValue("site.navbarItems", data.data.site.navbarItems);
|
||||||
|
form.setValue("site.supports", data.data.site.supports);
|
||||||
|
form.setValue("site.footer", data.data.site.footer);
|
||||||
|
|
||||||
|
toast.success(data.message, {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
setIsLoadingConfig(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,7 +189,7 @@ export default function ConfiguratorPage() {
|
|||||||
href={"https://themes.fkaya.dev/"}
|
href={"https://themes.fkaya.dev/"}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-balance text-sm text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
className='text-balance text-sm font-medium text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
||||||
>
|
>
|
||||||
themes.fkaya.dev
|
themes.fkaya.dev
|
||||||
</Link>
|
</Link>
|
||||||
@@ -139,7 +198,7 @@ export default function ConfiguratorPage() {
|
|||||||
href={"https://themeshadcn.com/"}
|
href={"https://themeshadcn.com/"}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-balance text-sm text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
className='text-balance text-sm font-medium text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
||||||
>
|
>
|
||||||
themeshadcn.com
|
themeshadcn.com
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@@ -148,7 +207,7 @@ export default function ConfiguratorPage() {
|
|||||||
href={"https://ui.jln.dev/"}
|
href={"https://ui.jln.dev/"}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-balance text-sm text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
className='text-balance text-sm font-medium text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
||||||
>
|
>
|
||||||
ui.jln.dev
|
ui.jln.dev
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@@ -157,47 +216,44 @@ export default function ConfiguratorPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className='flex w-full flex-col gap-4 tablet:flex-row tablet:items-center tablet:justify-between'>
|
||||||
<CardTitle>Configurator</CardTitle>
|
<div className='flex grow flex-col space-y-1.5'>
|
||||||
<CardDescription>Generate configurator for your index.</CardDescription>
|
<CardTitle>Configurator</CardTitle>
|
||||||
</CardHeader>
|
<CardDescription>Generate configurator for your index.</CardDescription>
|
||||||
<CardContent>
|
</div>
|
||||||
<div className='flex w-full flex-col items-center gap-2 pb-4 tablet:flex-row-reverse'>
|
<div className='flex flex-col items-center gap-2 tablet:flex-row-reverse'>
|
||||||
<ResponsiveDropdownMenu>
|
<LoadingButton
|
||||||
<ResponsiveDropdownMenuTrigger asChild>
|
loading={isLoadingConfig}
|
||||||
<Button className='w-full tablet:w-fit'>
|
className='w-full tablet:w-fit'
|
||||||
Load Config
|
onClick={() => {
|
||||||
<Icon name='ChevronsUpDown' />
|
setIsLoadingConfig(true);
|
||||||
</Button>
|
|
||||||
</ResponsiveDropdownMenuTrigger>
|
pickFile({
|
||||||
<ResponsiveDropdownMenuContent
|
accept: ".ts",
|
||||||
header={{
|
async onLoad(response) {
|
||||||
title: "Load Config",
|
await onLoadConfig(response);
|
||||||
description: "Load configuration from existing file",
|
},
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<ResponsiveDropdownMenuItem closeOnSelect>v2.4 / latest</ResponsiveDropdownMenuItem>
|
>
|
||||||
<ResponsiveDropdownMenuItem closeOnSelect>v2.3 / below</ResponsiveDropdownMenuItem>
|
Load Config
|
||||||
<ResponsiveDropdownMenuItem closeOnSelect>v1.x / legacy</ResponsiveDropdownMenuItem>
|
</LoadingButton>
|
||||||
</ResponsiveDropdownMenuContent>
|
<LoadingButton
|
||||||
</ResponsiveDropdownMenu>
|
loading={isLoadingEnv}
|
||||||
<ResponsiveDropdownMenu>
|
className='w-full tablet:w-fit'
|
||||||
<ResponsiveDropdownMenuTrigger asChild>
|
onClick={() => {
|
||||||
<Button className='w-full tablet:w-fit'>
|
setIsLoadingEnv(true);
|
||||||
Load Env
|
|
||||||
<Icon name='ChevronsUpDown' />
|
pickFile({
|
||||||
</Button>
|
accept: ".env",
|
||||||
</ResponsiveDropdownMenuTrigger>
|
async onLoad(response) {
|
||||||
<ResponsiveDropdownMenuContent
|
await onLoadEnv(response);
|
||||||
header={{
|
},
|
||||||
title: "Load Env",
|
});
|
||||||
description: "Load environment variables from existing file",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Load Env
|
||||||
<ResponsiveDropdownMenuItem closeOnSelect>v2.x / latest</ResponsiveDropdownMenuItem>
|
</LoadingButton>
|
||||||
<ResponsiveDropdownMenuItem closeOnSelect>v1.x / legacy</ResponsiveDropdownMenuItem>
|
|
||||||
</ResponsiveDropdownMenuContent>
|
|
||||||
</ResponsiveDropdownMenu>
|
|
||||||
<Button
|
<Button
|
||||||
className='w-full tablet:w-fit'
|
className='w-full tablet:w-fit'
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
@@ -206,11 +262,14 @@ export default function ConfiguratorPage() {
|
|||||||
Reset All
|
Reset All
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Form {...form}>
|
</CardHeader>
|
||||||
<form
|
<Separator className='mb-6' />
|
||||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
<Form {...form}>
|
||||||
className='grid grid-cols-1 gap-8 tablet:px-2'
|
<form
|
||||||
>
|
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||||
|
className='grid grid-cols-1'
|
||||||
|
>
|
||||||
|
<CardContent className='grid grid-cols-1 gap-8'>
|
||||||
<EnvironmentForm
|
<EnvironmentForm
|
||||||
form={form}
|
form={form}
|
||||||
onResetField={(field) => form.resetField(field)}
|
onResetField={(field) => form.resetField(field)}
|
||||||
@@ -229,9 +288,20 @@ export default function ConfiguratorPage() {
|
|||||||
form={form}
|
form={form}
|
||||||
onResetField={(field) => form.resetField(field)}
|
onResetField={(field) => form.resetField(field)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</CardContent>
|
||||||
</Form>
|
<CardFooter>
|
||||||
</CardContent>
|
<LoadingButton
|
||||||
|
size={"lg"}
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
disabled={!form.formState.isValid || !form.formState.isDirty}
|
||||||
|
type='submit'
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
Generate Configuration
|
||||||
|
</LoadingButton>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -262,8 +332,6 @@ type FormSectionProps = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
export function FormSection({ title, description, children }: PropsWithChildren<FormSectionProps>) {
|
export function FormSection({ title, description, children }: PropsWithChildren<FormSectionProps>) {
|
||||||
const { isDesktop } = useResponsive();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={title
|
id={title
|
||||||
|
|||||||
+441
-15
@@ -1,6 +1,171 @@
|
|||||||
import { z } from "zod";
|
import { type z } from "zod";
|
||||||
|
|
||||||
const configurationTemplate = `import { type z } from "zod";
|
import {
|
||||||
|
Schema_App_Configuration,
|
||||||
|
Schema_App_Configuration_Env,
|
||||||
|
Schema_Config,
|
||||||
|
Schema_v1_Config,
|
||||||
|
Schema_v2_3_Config,
|
||||||
|
} from "~/types/schema";
|
||||||
|
|
||||||
|
import config from "~/config/gIndex.config";
|
||||||
|
|
||||||
|
export const versionExpectMap: Record<"v1" | "v2" | "latest", string[]> = {
|
||||||
|
v1: ["1.0.0", "1.0.1", "1.0.2", "1.0.3"],
|
||||||
|
v2: ["2.0.0", "2.0.1", "2.0.2", "2.0.3"],
|
||||||
|
latest: ["2.0.4"],
|
||||||
|
};
|
||||||
|
export type PickFileResponse =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
message: string;
|
||||||
|
details: string[];
|
||||||
|
};
|
||||||
|
type PickFileProps = {
|
||||||
|
accept: string;
|
||||||
|
onLoad: (response: PickFileResponse) => void | Promise<void>;
|
||||||
|
onCancel?: () => void | Promise<void>;
|
||||||
|
fileName?: string;
|
||||||
|
};
|
||||||
|
export function pickFile({ accept, fileName, onLoad }: PickFileProps): void {
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.accept = accept;
|
||||||
|
fileInput.oncancel = async () => {
|
||||||
|
await onLoad({
|
||||||
|
success: false,
|
||||||
|
message: "File picker canceled",
|
||||||
|
details: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fileInput.onchange = async (fileEvent) => {
|
||||||
|
const file = (fileEvent.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
await onLoad({
|
||||||
|
success: false,
|
||||||
|
message: "No file selected",
|
||||||
|
details: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
const { name } = file;
|
||||||
|
if (name.toLowerCase() !== fileName.toLowerCase()) {
|
||||||
|
await onLoad({
|
||||||
|
success: false,
|
||||||
|
message: `Invalid file name`,
|
||||||
|
details: [`Expected: ${fileName}`, `Received: ${name}`],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (readerEvent) => {
|
||||||
|
const result = readerEvent.target?.result as string;
|
||||||
|
if (!result) {
|
||||||
|
await onLoad({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to read file",
|
||||||
|
details: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileInput.value = "";
|
||||||
|
|
||||||
|
await onLoad({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onloadend = () => {
|
||||||
|
fileInput.remove();
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
fileInput.click();
|
||||||
|
fileInput.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialConfiguration: z.input<typeof Schema_App_Configuration> = {
|
||||||
|
version: config.version,
|
||||||
|
environment: {
|
||||||
|
ENCRYPTION_KEY: "",
|
||||||
|
SITE_PASSWORD: "",
|
||||||
|
GD_SERVICE_B64: "",
|
||||||
|
NEXT_PUBLIC_DOMAIN: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
cache: {
|
||||||
|
public: true,
|
||||||
|
maxAge: 60,
|
||||||
|
sMaxAge: 60,
|
||||||
|
staleWhileRevalidate: true,
|
||||||
|
},
|
||||||
|
rootFolder: "",
|
||||||
|
isTeamDrive: false,
|
||||||
|
sharedDrive: "",
|
||||||
|
defaultQuery: ["trashed = false", "(not mimeType contains 'google-apps' or mimeType contains 'folder')"],
|
||||||
|
defaultField:
|
||||||
|
"id, name, mimeType, thumbnailLink, fileExtension, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, trashed",
|
||||||
|
defaultOrder: "folder, name asc, modifiedTime desc",
|
||||||
|
itemsPerPage: 50,
|
||||||
|
searchResult: 5,
|
||||||
|
proxyThumbnail: true,
|
||||||
|
streamMaxSize: 100 * 1024 * 1024,
|
||||||
|
specialFile: {
|
||||||
|
password: ".password",
|
||||||
|
readme: ".readme.md",
|
||||||
|
banner: ".banner",
|
||||||
|
},
|
||||||
|
hiddenFiles: [".password", ".readme.md", ".banner", ".banner.jpg", ".banner.png", ".banner.webp"],
|
||||||
|
allowDownloadProtectedFile: false,
|
||||||
|
temporaryTokenDuration: 6,
|
||||||
|
maxFileSize: 4 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
|
||||||
|
site: {
|
||||||
|
guideButton: false,
|
||||||
|
|
||||||
|
siteName: "next-gdrive-index",
|
||||||
|
siteNameTemplate: "%s - %t",
|
||||||
|
siteDescription: "A simple file browser for Google Drive",
|
||||||
|
siteIcon: "/logo.svg",
|
||||||
|
siteAuthor: "mbaharip",
|
||||||
|
favIcon: "/favicon.png",
|
||||||
|
|
||||||
|
robots: "noindex, nofollow",
|
||||||
|
twitterHandle: "@mbaharip_",
|
||||||
|
showFileExtension: true,
|
||||||
|
footer: [
|
||||||
|
{ value: "{{ poweredBy }}" },
|
||||||
|
{ value: "Made with ❤️ by [**{{ author }}**](https://github.com/mbaharip)" },
|
||||||
|
],
|
||||||
|
experimental_pageLoadTime: false,
|
||||||
|
privateIndex: false,
|
||||||
|
breadcrumbMax: 3,
|
||||||
|
toaster: {
|
||||||
|
position: "bottom-right",
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
navbarItems: [],
|
||||||
|
supports: [],
|
||||||
|
previewSettings: {
|
||||||
|
manga: {
|
||||||
|
maxSize: 15 * 1024 * 1024,
|
||||||
|
maxItem: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const configurationTemplate = `import { type z } from "zod";
|
||||||
import { BASE_URL } from "~/constant";
|
import { BASE_URL } from "~/constant";
|
||||||
|
|
||||||
import { type Schema_Config } from "~/types/schema";
|
import { type Schema_Config } from "~/types/schema";
|
||||||
@@ -293,7 +458,7 @@ const config: z.input<typeof Schema_Config> = {
|
|||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
`;
|
`;
|
||||||
const environmentTemplate = `# Base64 Encoded Service Account JSON
|
export const environmentTemplate = `# Base64 Encoded Service Account JSON
|
||||||
GD_SERVICE_B64={{ serviceAccount }}
|
GD_SERVICE_B64={{ serviceAccount }}
|
||||||
# Secret Key for Encryption
|
# Secret Key for Encryption
|
||||||
ENCRYPTION_KEY={{ key }}
|
ENCRYPTION_KEY={{ key }}
|
||||||
@@ -304,18 +469,279 @@ SITE_PASSWORD={{ password }}
|
|||||||
# Needed if you're not using Vercel
|
# Needed if you're not using Vercel
|
||||||
NEXT_PUBLIC_DOMAIN={{ domain }}`;
|
NEXT_PUBLIC_DOMAIN={{ domain }}`;
|
||||||
|
|
||||||
const version1Schema = z.object({});
|
const version1Schema = Schema_v1_Config;
|
||||||
const version2Schema = z.object({});
|
const version2Schema = Schema_v2_3_Config;
|
||||||
const newVersion2Schema = z.object({});
|
const newVersion2Schema = Schema_Config;
|
||||||
const environmentSchema = z.object({
|
const formSchema = Schema_App_Configuration.omit({ environment: true });
|
||||||
GD_SERVICE_B64: z.string(),
|
const latestEnvironmentSchema = Schema_App_Configuration_Env;
|
||||||
ENCRYPTION_KEY: z.string(),
|
|
||||||
SITE_PASSWORD: z.string().optional(),
|
|
||||||
NEXT_PUBLIC_DOMAIN: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function parseVersion1Config(configuration: string) {}
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type ConfigurationResponse<T extends z.ZodObject<any>> =
|
||||||
|
| z.infer<T>
|
||||||
|
| {
|
||||||
|
message: string;
|
||||||
|
details: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export function parseVersion2Config(configuration: string) {}
|
export function parseVersion1Config(configuration: string) {
|
||||||
|
const config = (configuration.split(/const config:\s.*?=\s/g)[1]?.split("export default config;")[0] ?? "")
|
||||||
|
|
||||||
export function parseEnvironmentConfig(configuration: string) {}
|
.replace(/\\/g, "") // Remove all escape backslashes
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove all multi-line comments
|
||||||
|
.replace(/,\s\/\/\s.*/g, ",") // Remove comments after values
|
||||||
|
.replace(/[^,]\/\/\s.*?,/g, "") // Remove single line comments
|
||||||
|
|
||||||
|
.replace(/\r\n/g, "")
|
||||||
|
.replace(/\n/g, "")
|
||||||
|
.replace(/\t/g, "") // Remove line breaks and tabs
|
||||||
|
|
||||||
|
.replace(/basePath:(.*?),/g, 'basePath: "placeholder-domain",') // Replace basePath variable with placeholder
|
||||||
|
.replace(/maxFileSize:(.*?),/g, (str) => {
|
||||||
|
if (!str) return "maxFileSize: 4194304,"; // Set maxFileSize to 4MB
|
||||||
|
const value = str?.split(":")[1]?.split(",")[0]?.trim() ?? "";
|
||||||
|
const numbers = value.split("*").map((v) => Number(v ?? "1"));
|
||||||
|
return `maxFileSize: ${numbers.reduce((a, b) => a * b, 1)}`;
|
||||||
|
})
|
||||||
|
|
||||||
|
.replace(/([a-zA-Z]*?):\s/g, '"$1": ') // Add double quotes to keys
|
||||||
|
.replace(/\s{2,4}|/g, "") // Replace all double+ spaces with single space
|
||||||
|
.replace(/,(?=[^,]*$)/, "") // Remove trailing comma
|
||||||
|
.replace(/(,\])/g, "]") // Remove trailing comma before closing bracket
|
||||||
|
.replace(/(,\})/g, "}") // Remove trailing comma before closing brace
|
||||||
|
.slice(0, -1);
|
||||||
|
|
||||||
|
const json = JSON.parse(config) as object;
|
||||||
|
const parsedJson = version1Schema.safeParse(json);
|
||||||
|
if (!parsedJson.success) {
|
||||||
|
return {
|
||||||
|
message: "Failed to match the version 1 schema",
|
||||||
|
details: parsedJson.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = parsedJson;
|
||||||
|
|
||||||
|
const migratedData: z.input<typeof formSchema> = {
|
||||||
|
version: data.version,
|
||||||
|
api: {
|
||||||
|
...initialConfiguration.api,
|
||||||
|
cache: {
|
||||||
|
public: data.cacheControl.includes("public"),
|
||||||
|
maxAge: Number(/max-age=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
sMaxAge: Number(/s-maxage=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
staleWhileRevalidate: data.cacheControl.includes("stale-while-revalidate"),
|
||||||
|
},
|
||||||
|
rootFolder: data.apiConfig.rootFolder,
|
||||||
|
isTeamDrive: false,
|
||||||
|
sharedDrive: "",
|
||||||
|
itemsPerPage: data.apiConfig.itemsPerPage,
|
||||||
|
searchResult: data.apiConfig.searchResult,
|
||||||
|
specialFile: {
|
||||||
|
password: data.apiConfig.specialFile.password,
|
||||||
|
readme: data.apiConfig.specialFile.readme,
|
||||||
|
banner: data.apiConfig.specialFile.banner,
|
||||||
|
},
|
||||||
|
hiddenFiles: data.apiConfig.hiddenFiles,
|
||||||
|
allowDownloadProtectedFile: data.apiConfig.allowDownloadProtectedFile,
|
||||||
|
temporaryTokenDuration: data.apiConfig.temporaryTokenDuration,
|
||||||
|
maxFileSize: data.apiConfig.maxFileSize,
|
||||||
|
},
|
||||||
|
site: {
|
||||||
|
...initialConfiguration.site,
|
||||||
|
guideButton: false,
|
||||||
|
siteName: data.siteConfig.siteName,
|
||||||
|
siteDescription: data.siteConfig.siteDescription,
|
||||||
|
twitterHandle: data.siteConfig.twitterHandle ?? initialConfiguration.site.twitterHandle ?? "@__mbaharip__",
|
||||||
|
privateIndex: data.siteConfig.privateIndex,
|
||||||
|
navbarItems: data.siteConfig.navbarItems.map((item) => ({
|
||||||
|
icon: "File",
|
||||||
|
name: item.name,
|
||||||
|
href: item.href,
|
||||||
|
external: item.external ?? false,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedData = formSchema.safeParse(migratedData);
|
||||||
|
if (!parsedData.success) {
|
||||||
|
return {
|
||||||
|
message: "Failed to migrate the old configuration to the new schema",
|
||||||
|
details: parsedData.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedData.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersion2Config(configuration: string) {
|
||||||
|
const isLatest = configuration.includes('version: "2.0.4"');
|
||||||
|
const config = (configuration.split(/const config:\s.*?=\s/g)[1]?.split("export default config;")[0] ?? "")
|
||||||
|
|
||||||
|
.replace(/\\/g, "") // Remove all escape backslashes
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove all multi-line comments
|
||||||
|
.replace(/,\s\/\/\s.*/g, ",") // Remove comments after values
|
||||||
|
.replace(/[^,]\/\/\s.*?,/g, "") // Remove single line comments
|
||||||
|
|
||||||
|
.replace(/\r\n/g, "")
|
||||||
|
.replace(/\n/g, "")
|
||||||
|
.replace(/\t/g, "") // Remove line breaks and tabs
|
||||||
|
|
||||||
|
.replace(/basePath:(.*?),/g, 'basePath: "placeholder-domain",') // Replace basePath variable with placeholder
|
||||||
|
.replace(/streamMaxSize:(.*?),/g, (str) => {
|
||||||
|
if (!str) return "streamMaxSize: 104857600,"; // Set streamMaxSize to 100MB
|
||||||
|
const value = str?.split(":")[1]?.split(",")[0]?.trim() ?? "";
|
||||||
|
const numbers = value.split("*").map((v) => Number(v ?? "1"));
|
||||||
|
return `streamMaxSize: ${numbers.reduce((a, b) => a * b, 1)},`;
|
||||||
|
})
|
||||||
|
.replace(/temporaryTokenDuration:(.*?),/g, (str) => {
|
||||||
|
if (!str) return "temporaryTokenDuration: 6,"; // Set temporaryTokenDuration to 6 hours
|
||||||
|
const value = str?.split(":")[1]?.split(",")[0]?.trim() ?? "";
|
||||||
|
if (value.includes("/")) {
|
||||||
|
const numbers = value.split("/").map((v) => Number(v ?? "1"));
|
||||||
|
return `temporaryTokenDuration: ${numbers.reduce((a, b) => a / b, 1)},`;
|
||||||
|
} else {
|
||||||
|
return `temporaryTokenDuration: ${value},`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.replace(/maxFileSize:(.*?),/g, (str) => {
|
||||||
|
if (!str) return "maxFileSize: 4194304,"; // Set maxFileSize to 4MB
|
||||||
|
const value = str?.split(":")[1]?.split(",")[0]?.trim() ?? "";
|
||||||
|
const numbers = value.split("*").map((v) => Number(v ?? "1"));
|
||||||
|
return `maxFileSize: ${numbers.reduce((a, b) => a * b, 1)},`;
|
||||||
|
})
|
||||||
|
.replace(/maxSize:(.*?),/g, (str) => {
|
||||||
|
if (!str) return "maxSize: 15728640,"; // Set maxSize to 15MB
|
||||||
|
const value = str?.split(":")[1]?.split(",")[0]?.trim() ?? "";
|
||||||
|
const numbers = value.split("*").map((v) => Number(v ?? "1"));
|
||||||
|
return `maxSize: ${numbers.reduce((a, b) => a * b, 1)},`;
|
||||||
|
})
|
||||||
|
|
||||||
|
.replace(/([a-zA-Z_]*?):\s/g, '"$1": ') // Add double quotes to keys
|
||||||
|
.replace(/\s{2,4}|/g, "") // Replace all double+ spaces with single space
|
||||||
|
.replace(/,(?=[^,]*$)/, "") // Remove trailing comma
|
||||||
|
.replace(/(,\])/g, "]") // Remove trailing comma before closing bracket
|
||||||
|
.replace(/(,\})/g, "}") // Remove trailing comma before closing brace
|
||||||
|
.slice(0, -1);
|
||||||
|
|
||||||
|
const json = JSON.parse(config) as object;
|
||||||
|
if (isLatest) {
|
||||||
|
const parsedJson = newVersion2Schema.safeParse(json);
|
||||||
|
if (!parsedJson.success) {
|
||||||
|
return {
|
||||||
|
message: `Failed to match the schema for ${isLatest ? "latest" : "version 2.3 / below"} configuration`,
|
||||||
|
details: parsedJson.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = parsedJson;
|
||||||
|
|
||||||
|
const migratedData: z.input<typeof formSchema> = {
|
||||||
|
version: data.version,
|
||||||
|
api: {
|
||||||
|
...initialConfiguration.api,
|
||||||
|
...data.apiConfig,
|
||||||
|
rootFolder: "",
|
||||||
|
isTeamDrive: false,
|
||||||
|
sharedDrive: "",
|
||||||
|
cache: {
|
||||||
|
public: data.cacheControl.includes("public"),
|
||||||
|
maxAge: Number(/max-age=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
sMaxAge: Number(/s-maxage=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
staleWhileRevalidate: data.cacheControl.includes("stale-while-revalidate"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
site: {
|
||||||
|
...initialConfiguration.site,
|
||||||
|
...data.siteConfig,
|
||||||
|
guideButton: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedData = formSchema.safeParse(migratedData);
|
||||||
|
if (!parsedData.success) {
|
||||||
|
return {
|
||||||
|
message: "Failed to migrate the old configuration to the new schema",
|
||||||
|
details: parsedData.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedData.data;
|
||||||
|
} else {
|
||||||
|
const parsedJson = version2Schema.safeParse(json);
|
||||||
|
if (!parsedJson.success) {
|
||||||
|
return {
|
||||||
|
message: `Failed to match the schema for ${isLatest ? "latest" : "version 2.3 / below"} configuration`,
|
||||||
|
details: parsedJson.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = parsedJson;
|
||||||
|
|
||||||
|
const migratedData: z.input<typeof formSchema> = {
|
||||||
|
version: data.version,
|
||||||
|
api: {
|
||||||
|
...initialConfiguration.api,
|
||||||
|
rootFolder: "",
|
||||||
|
isTeamDrive: false,
|
||||||
|
sharedDrive: "",
|
||||||
|
cache: {
|
||||||
|
public: data.cacheControl.includes("public"),
|
||||||
|
maxAge: Number(/max-age=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
sMaxAge: Number(/s-maxage=(\d+)/.exec(data.cacheControl)?.[1] ?? 60),
|
||||||
|
staleWhileRevalidate: data.cacheControl.includes("stale-while-revalidate"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
site: {
|
||||||
|
...initialConfiguration.site,
|
||||||
|
...data.siteConfig,
|
||||||
|
footer: (data.siteConfig.footer ?? []).map((item) => ({ value: item })),
|
||||||
|
guideButton: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedData = formSchema.safeParse(migratedData);
|
||||||
|
if (!parsedData.success) {
|
||||||
|
return {
|
||||||
|
message: "Failed to migrate the old configuration to the new schema",
|
||||||
|
details: parsedData.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedData.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEnvironment(configuration: string): ConfigurationResponse<typeof latestEnvironmentSchema> {
|
||||||
|
const lines = configuration.split("\n");
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
const availableKeys = [
|
||||||
|
"GD_SERVICE_B64",
|
||||||
|
"ENCRYPTION_KEY",
|
||||||
|
"SITE_PASSWORD",
|
||||||
|
"NEXT_PUBLIC_DOMAIN",
|
||||||
|
"NEXT_PUBLIC_VERCEL_URL",
|
||||||
|
"NEXT_PUBLIC_ENCRYPTION_KEY",
|
||||||
|
"NEXT_PUBLIC_SITE_PASSWORD",
|
||||||
|
];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!availableKeys.some((key) => line.startsWith(key))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, value] = line.split("=");
|
||||||
|
if (!key || !value) continue;
|
||||||
|
const formattedValue = value.replace(/"/g, "").trim();
|
||||||
|
const formattedKey = ["NEXT_PUBLIC_ENCRYPTION_KEY", "NEXT_PUBLIC_SITE_PASSWORD"].includes(key)
|
||||||
|
? key.replace("NEXT_PUBLIC_", "")
|
||||||
|
: key;
|
||||||
|
result[formattedKey] = formattedValue;
|
||||||
|
}
|
||||||
|
const validated = latestEnvironmentSchema.safeParse(result);
|
||||||
|
if (!validated.success)
|
||||||
|
return {
|
||||||
|
message: "Failed to match the latest environment schema",
|
||||||
|
details: validated.error.errors.map((error) => `[${error.path.join(".")}] ${error.message}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
return validated.data;
|
||||||
|
}
|
||||||
|
|||||||
+19
-10
@@ -64,11 +64,17 @@ export const base64Encode = (text: string, type: B64Type = "url") => {
|
|||||||
if (type === "standard") return encodeBase64(data);
|
if (type === "standard") return encodeBase64(data);
|
||||||
return encodeBase64url(data);
|
return encodeBase64url(data);
|
||||||
};
|
};
|
||||||
export const base64Decode = <T = unknown>(encoded: string, type: B64Type = "url"): T => {
|
export const base64Decode = <T = unknown>(encoded: string, type: B64Type = "url"): T | null => {
|
||||||
let decoded: Uint8Array<ArrayBufferLike>;
|
try {
|
||||||
if (type === "standard") decoded = decodeBase64(encoded);
|
let decoded: Uint8Array<ArrayBufferLike>;
|
||||||
else decoded = decodeBase64url(encoded);
|
if (type === "standard") decoded = decodeBase64(encoded);
|
||||||
return new TextDecoder().decode(decoded) as T;
|
else decoded = decodeBase64url(encoded);
|
||||||
|
return new TextDecoder().decode(decoded) as T;
|
||||||
|
} catch (error) {
|
||||||
|
const e = error as Error;
|
||||||
|
console.error(`[base64Decode] ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type ServiceAccount = {
|
type ServiceAccount = {
|
||||||
@@ -90,15 +96,18 @@ class GoogleDriveService {
|
|||||||
public gdriveNoCache: drive_v3.Drive;
|
public gdriveNoCache: drive_v3.Drive;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const decodedBase64 = JSON.parse(base64Decode<string>(process.env.GD_SERVICE_B64!)) as ServiceAccount;
|
const decodedB64 = base64Decode<string>(process.env.GD_SERVICE_B64!);
|
||||||
|
if (!decodedB64) throw new Error("Failed to decode GD_SERVICE_B64");
|
||||||
|
const parsedAuth = JSON.parse(decodedB64) as ServiceAccount;
|
||||||
|
|
||||||
this.auth = new google.auth.GoogleAuth({
|
this.auth = new google.auth.GoogleAuth({
|
||||||
credentials: {
|
credentials: {
|
||||||
type: "service_account",
|
type: "service_account",
|
||||||
private_key: decodedBase64.private_key,
|
private_key: parsedAuth.private_key,
|
||||||
client_email: decodedBase64.client_email,
|
client_email: parsedAuth.client_email,
|
||||||
client_id: decodedBase64.client_id,
|
client_id: parsedAuth.client_id,
|
||||||
},
|
},
|
||||||
projectId: decodedBase64.project_id,
|
projectId: parsedAuth.project_id,
|
||||||
scopes: ["https://www.googleapis.com/auth/drive"],
|
scopes: ["https://www.googleapis.com/auth/drive"],
|
||||||
});
|
});
|
||||||
if (!this.auth) throw new Error("Failed to initialize Google Auth");
|
if (!this.auth) throw new Error("Failed to initialize Google Auth");
|
||||||
|
|||||||
+69
-11
@@ -40,7 +40,7 @@ export const Schema_File = z.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Schema_Old_Config = z.object({
|
export const Schema_v1_Config = z.object({
|
||||||
version: z.literal("1.0.0"),
|
version: z.literal("1.0.0"),
|
||||||
basePath: z.string(),
|
basePath: z.string(),
|
||||||
masterKey: z.string(),
|
masterKey: z.string(),
|
||||||
@@ -54,7 +54,7 @@ export const Schema_Old_Config = z.object({
|
|||||||
defaultField: z.string(),
|
defaultField: z.string(),
|
||||||
defaultOrder: z.string(),
|
defaultOrder: z.string(),
|
||||||
itemsPerPage: z.number().positive(),
|
itemsPerPage: z.number().positive(),
|
||||||
searchRsult: z.number().positive(),
|
searchResult: z.number().positive(),
|
||||||
|
|
||||||
specialFile: z.object({
|
specialFile: z.object({
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
@@ -116,10 +116,56 @@ export const Schema_Config_API = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.isTeamDrive && !data.sharedDrive) return false;
|
if (data.isTeamDrive === true && !data.sharedDrive) return false;
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
{ message: "sharedDrive is required when isTeamDrive is true" },
|
{ message: "sharedDrive is required when isTeamDrive is true" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const Schema_v2_3_Config_Site = z.object({
|
||||||
|
siteName: z.string(),
|
||||||
|
siteNameTemplate: z.string().optional().default("%s"),
|
||||||
|
siteDescription: z.string(),
|
||||||
|
siteIcon: z.string(),
|
||||||
|
siteAuthor: z.string().optional().default("mbaharip"),
|
||||||
|
favIcon: z.string(),
|
||||||
|
robots: z.string().optional().default("noindex, nofollow"),
|
||||||
|
twitterHandle: z.string().optional().default("@__mbaharip__"),
|
||||||
|
|
||||||
|
showFileExtension: z.boolean().optional().default(false),
|
||||||
|
|
||||||
|
footer: z.string().array().optional(),
|
||||||
|
|
||||||
|
privateIndex: z.boolean().optional().default(false),
|
||||||
|
breadcrumbMax: z.number(),
|
||||||
|
|
||||||
|
toaster: z
|
||||||
|
.object({
|
||||||
|
position: z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]),
|
||||||
|
duration: z.number().positive(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({
|
||||||
|
position: "top-right",
|
||||||
|
duration: 5000,
|
||||||
|
}),
|
||||||
|
|
||||||
|
navbarItems: z.array(
|
||||||
|
z.object({
|
||||||
|
icon: z.enum(Object.keys(icons) as [keyof typeof icons]),
|
||||||
|
name: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
external: z.boolean().optional().default(false),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
supports: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
href: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
export const Schema_Config_Site = z.object({
|
export const Schema_Config_Site = z.object({
|
||||||
siteName: z.string(),
|
siteName: z.string(),
|
||||||
siteNameTemplate: z.string().optional().default("%s"),
|
siteNameTemplate: z.string().optional().default("%s"),
|
||||||
@@ -181,13 +227,17 @@ export const Schema_Config_Site = z.object({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export const Schema_App_Configuration_Env = z.object({
|
|
||||||
GD_SERVICE_B64: z.string(),
|
|
||||||
ENCRYPTION_KEY: z.string(),
|
|
||||||
SITE_PASSWORD: z.string().optional(),
|
|
||||||
NEXT_PUBLIC_DOMAIN: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export const Schema_v2_3_Config = z.object({
|
||||||
|
version: z.string(),
|
||||||
|
basePath: z.string(),
|
||||||
|
cacheControl: z.string(),
|
||||||
|
showDeployGuide: z.boolean(),
|
||||||
|
|
||||||
|
apiConfig: Schema_Config_API,
|
||||||
|
|
||||||
|
siteConfig: Schema_v2_3_Config_Site,
|
||||||
|
});
|
||||||
export const Schema_Config = z.object({
|
export const Schema_Config = z.object({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
basePath: z.string(),
|
basePath: z.string(),
|
||||||
@@ -199,6 +249,13 @@ export const Schema_Config = z.object({
|
|||||||
siteConfig: Schema_Config_Site,
|
siteConfig: Schema_Config_Site,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const Schema_App_Configuration_Env = z.object({
|
||||||
|
GD_SERVICE_B64: z.string(),
|
||||||
|
ENCRYPTION_KEY: z.string(),
|
||||||
|
SITE_PASSWORD: z.string().optional(),
|
||||||
|
NEXT_PUBLIC_DOMAIN: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Schema_ServiceAccount = z.object({
|
export const Schema_ServiceAccount = z.object({
|
||||||
type: z.literal("service_account"),
|
type: z.literal("service_account"),
|
||||||
project_id: z.string(),
|
project_id: z.string(),
|
||||||
@@ -214,13 +271,14 @@ export const Schema_ServiceAccount = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const Schema_App_Configuration = z.object({
|
export const Schema_App_Configuration = z.object({
|
||||||
|
version: z.string(),
|
||||||
environment: Schema_App_Configuration_Env,
|
environment: Schema_App_Configuration_Env,
|
||||||
api: Schema_Config_API.and(
|
api: Schema_Config_API.and(
|
||||||
z.object({
|
z.object({
|
||||||
cache: z.object({
|
cache: z.object({
|
||||||
public: z.coerce.boolean(),
|
public: z.coerce.boolean(),
|
||||||
maxAge: z.coerce.number().positive(),
|
maxAge: z.coerce.number().min(0),
|
||||||
sMaxAge: z.coerce.number().positive(),
|
sMaxAge: z.coerce.number().min(0),
|
||||||
staleWhileRevalidate: z.coerce.boolean(),
|
staleWhileRevalidate: z.coerce.boolean(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user