mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 11:12:06 +10:00
fix: wip
This commit is contained in:
@ -37,7 +37,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type CreatePasskeyDialogProps = {
|
||||
export type PasskeyCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
onSuccess?: () => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@ -50,7 +50,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;
|
||||
|
||||
const parser = new UAParser();
|
||||
|
||||
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
|
||||
export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
@ -7,7 +7,7 @@ import { FilePlus, Loader } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -48,27 +48,7 @@ export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogP
|
||||
setIsUploadingFile(true);
|
||||
|
||||
try {
|
||||
// Todo
|
||||
// const { type, data } = await putPdfFile(file);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(async (res) => await res.json())
|
||||
.catch((e) => {
|
||||
console.error('Upload failed:', e);
|
||||
throw new AppError('UPLOAD_FAILED');
|
||||
});
|
||||
|
||||
// Why do we run this twice?
|
||||
// const { id: templateDocumentDataId } = await createDocumentData({
|
||||
// type: response.type,
|
||||
// data: response.data,
|
||||
// });
|
||||
const response = await putPdfFile(file);
|
||||
|
||||
const { id } = await createTemplate({
|
||||
title: file.name,
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||
} from '@documenso/lib/constants/template';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -150,22 +151,7 @@ export function TemplateUseDialog({
|
||||
let customDocumentDataId: string | undefined = undefined;
|
||||
|
||||
if (data.useCustomDocument && data.customDocumentData) {
|
||||
// const customDocumentData = await putPdfFile(data.customDocumentData);
|
||||
// Todo
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.customDocumentData);
|
||||
|
||||
const customDocumentData = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(async (res) => await res.json())
|
||||
.catch((e) => {
|
||||
console.error('Upload failed:', e);
|
||||
throw new AppError('UPLOAD_FAILED');
|
||||
});
|
||||
|
||||
const customDocumentData = await putPdfFile(data.customDocumentData);
|
||||
customDocumentDataId = customDocumentData.id;
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,12 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -38,7 +38,7 @@ export type TPasswordFormSchema = z.infer<typeof ZPasswordFormSchema>;
|
||||
|
||||
export type PasswordFormProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
user: SessionUser;
|
||||
};
|
||||
|
||||
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||
|
||||
@ -97,7 +97,7 @@ export const SignInForm = ({
|
||||
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
|
||||
const redirectUrl = useMemo(() => {
|
||||
const redirectPath = useMemo(() => {
|
||||
// Handle SSR
|
||||
if (typeof window === 'undefined') {
|
||||
return LOGIN_REDIRECT_PATH;
|
||||
@ -171,7 +171,7 @@ export const SignInForm = ({
|
||||
await authClient.passkey.signIn({
|
||||
credential: JSON.stringify(credential),
|
||||
csrfToken: sessionId,
|
||||
redirectUrl,
|
||||
redirectPath,
|
||||
});
|
||||
} catch (err) {
|
||||
setIsPasskeyLoading(false);
|
||||
@ -211,7 +211,7 @@ export const SignInForm = ({
|
||||
password,
|
||||
totpCode,
|
||||
backupCode,
|
||||
redirectUrl,
|
||||
redirectPath,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
@ -9,6 +9,8 @@ import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -78,8 +80,7 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
|
||||
let uploadedBrandingLogo = settings?.brandingLogo;
|
||||
|
||||
if (brandingLogo) {
|
||||
// Todo
|
||||
// uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
|
||||
uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
|
||||
}
|
||||
|
||||
if (brandingLogo === null) {
|
||||
@ -116,26 +117,12 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
|
||||
const file = JSON.parse(settings.brandingLogo);
|
||||
|
||||
if ('type' in file && 'data' in file) {
|
||||
// Todo
|
||||
// Todo
|
||||
// Todo
|
||||
void fetch(`/api/file?key=${file.key}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(async (res) => await res.json())
|
||||
.then((data) => {
|
||||
const objectUrl = URL.createObjectURL(new Blob([data.binaryData]));
|
||||
void getFile(file).then((binaryData) => {
|
||||
const objectUrl = URL.createObjectURL(new Blob([binaryData]));
|
||||
|
||||
setPreviewUrl(objectUrl);
|
||||
setHasLoadedPreview(true);
|
||||
});
|
||||
|
||||
// void getFile(file).then((binaryData) => {
|
||||
// const objectUrl = URL.createObjectURL(new Blob([binaryData]));
|
||||
|
||||
// setPreviewUrl(objectUrl);
|
||||
// setHasLoadedPreview(true);
|
||||
// });
|
||||
setPreviewUrl(objectUrl);
|
||||
setHasLoadedPreview(true);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||
|
||||
import type { User } from '@prisma/client';
|
||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||
import { Link, useLocation, useParams } from 'react-router';
|
||||
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { getRootHref } from '@documenso/lib/utils/params';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -16,7 +16,7 @@ import { AppNavMobile } from './app-nav-mobile';
|
||||
import { MenuSwitcher } from './menu-switcher';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
user: User;
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
|
||||
@ -109,14 +109,12 @@ export const DocumentSigningAuth2FA = ({
|
||||
)}
|
||||
</p>
|
||||
|
||||
{user?.identityProvider === 'DOCUMENSO' && (
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||
every time you sign in.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||
every time you sign in using email password.
|
||||
</Trans>
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<DialogFooter>
|
||||
|
||||
@ -35,7 +35,7 @@ export const DocumentSigningAuthAccount = ({
|
||||
setIsSigningOut(true);
|
||||
|
||||
await authClient.signOut({
|
||||
redirectUrl: `/signin#email=${email}`,
|
||||
redirectPath: `/signin#email=${email}`,
|
||||
});
|
||||
} catch {
|
||||
setIsSigningOut(false);
|
||||
|
||||
@ -27,7 +27,7 @@ export const DocumentSigningAuthPageView = ({
|
||||
setIsSigningOut(true);
|
||||
|
||||
await authClient.signOut({
|
||||
redirectUrl: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
|
||||
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
|
||||
@ -32,7 +32,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
import { CreatePasskeyDialog } from '~/components/dialogs/create-passkey-dialog';
|
||||
import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
|
||||
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
|
||||
@ -190,7 +190,7 @@ export const DocumentSigningAuthPasskey = ({
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<CreatePasskeyDialog
|
||||
<PasskeyCreateDialog
|
||||
onSuccess={async () => refetchPasskeys()}
|
||||
trigger={
|
||||
<Button>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { type Document, FieldType, type Passkey, type Recipient, type User } from '@prisma/client';
|
||||
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||
import type {
|
||||
TDocumentAuthOptions,
|
||||
@ -40,7 +41,7 @@ export type DocumentSigningAuthContextValue = {
|
||||
passkeyData: PasskeyData;
|
||||
preferredPasskeyId: string | null;
|
||||
setPreferredPasskeyId: (_value: string | null) => void;
|
||||
user?: User | null;
|
||||
user?: SessionUser | null;
|
||||
refetchPasskeys: () => Promise<void>;
|
||||
};
|
||||
|
||||
@ -63,7 +64,7 @@ export const useRequiredDocumentSigningAuthContext = () => {
|
||||
export interface DocumentSigningAuthProviderProps {
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Recipient;
|
||||
user?: User | null;
|
||||
user?: SessionUser | null;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ import type { HTMLAttributes } from 'react';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { ExtendedDocumentStatus } from '@prisma/types/extended-document-status';
|
||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -62,30 +63,11 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Todo
|
||||
// const { type, data } = await putPdfFile(file);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(async (res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error('Upload failed:', e);
|
||||
throw new AppError('UPLOAD_FAILED');
|
||||
});
|
||||
|
||||
// const { id: documentDataId } = await createDocumentData({
|
||||
// type,
|
||||
// data,
|
||||
// });
|
||||
const response = await putPdfFile(file);
|
||||
|
||||
const { id } = await createDocument({
|
||||
title: file.name,
|
||||
documentDataId: response.id, // todo
|
||||
documentDataId: response.id,
|
||||
timezone: userTimezone,
|
||||
});
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ import { useState } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
|
||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
@ -32,7 +32,7 @@ import {
|
||||
const MotionLink = motion(Link);
|
||||
|
||||
export type MenuSwitcherProps = {
|
||||
user: User;
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
|
||||
|
||||
export const UpcomingProfileClaimTeaser = () => {
|
||||
const { user } = useSession();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [claimed, setClaimed] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open && !claimed) {
|
||||
toast({
|
||||
title: _(msg`Claim your profile later`),
|
||||
description: _(
|
||||
msg`You can claim your profile later on by going to your profile settings!`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
localStorage.setItem('app.hasShownProfileClaimDialog', 'true');
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[claimed, toast],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hasShownProfileClaimDialog =
|
||||
localStorage.getItem('app.hasShownProfileClaimDialog') === 'true';
|
||||
|
||||
if (!user.url && !hasShownProfileClaimDialog) {
|
||||
onOpenChange(true);
|
||||
}
|
||||
}, [onOpenChange, user.url]);
|
||||
|
||||
return (
|
||||
<ClaimPublicProfileDialogForm
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onClaimed={() => setClaimed(true)}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -5,8 +5,8 @@ import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { ONE_DAY, ONE_SECOND } from '@documenso/lib/constants/time';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -27,18 +27,20 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
|
||||
|
||||
// Todo
|
||||
const { mutateAsync: sendConfirmationEmail, isPending } =
|
||||
trpc.profile.sendConfirmationEmail.useMutation();
|
||||
|
||||
const onResendConfirmationEmail = async () => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
setIsButtonDisabled(true);
|
||||
|
||||
await sendConfirmationEmail({ email: email });
|
||||
await authClient.emailPassword.resendVerifyEmail({ email: email });
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
@ -56,6 +58,8 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -196,7 +196,7 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo
|
||||
/>
|
||||
)}
|
||||
|
||||
<DocumentResendDialog document={row} recipients={nonSignedRecipients} team={team} />
|
||||
<DocumentResendDialog document={row} recipients={nonSignedRecipients} />
|
||||
|
||||
<DocumentShareButton
|
||||
documentId={row.id}
|
||||
|
||||
@ -180,5 +180,11 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
|
||||
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
console.log(error.data);
|
||||
console.log(error.status);
|
||||
console.log(error.statusText);
|
||||
}
|
||||
|
||||
return <GenericErrorLayout errorCode={errorCode} />;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import type { Route } from './+types/_index';
|
||||
import type { Route } from './+types/subscriptions';
|
||||
|
||||
export async function loader() {
|
||||
const subscriptions = await findSubscriptions();
|
||||
|
||||
@ -6,7 +6,7 @@ import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
|
||||
|
||||
import { AdminDashboardUsersTable } from '~/components/tables/admin-dashboard-users-table';
|
||||
|
||||
import type { Route } from './+types/users';
|
||||
import type { Route } from './+types/users._index';
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
@ -137,7 +137,8 @@ export default function DocumentPage() {
|
||||
|
||||
const { recipients, documentData, documentMeta } = document;
|
||||
|
||||
const isDocumentHistoryEnabled = false; // Todo: Was flag
|
||||
// This was a feature flag. Leave to false since it's not ready.
|
||||
const isDocumentHistoryEnabled = false;
|
||||
|
||||
return (
|
||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||
|
||||
@ -60,7 +60,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const user = useSession();
|
||||
const { user } = useSession();
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [isPublicProfileVisible, setIsPublicProfileVisible] = useState(profile.enabled);
|
||||
|
||||
@ -2,8 +2,10 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Link } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@ -13,22 +15,46 @@ import { ViewRecoveryCodesDialog } from '~/components/forms/2fa/view-recovery-co
|
||||
import { PasswordForm } from '~/components/forms/password';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
|
||||
import type { Route } from './+types';
|
||||
|
||||
export function meta() {
|
||||
return [{ title: 'Security' }];
|
||||
}
|
||||
|
||||
export default function SettingsSecurity() {
|
||||
export async function loader() {
|
||||
const { user } = getLoaderSession();
|
||||
|
||||
const accounts = await prisma.account.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
provider: true,
|
||||
},
|
||||
});
|
||||
|
||||
const providers = accounts.map((account) => account.provider);
|
||||
|
||||
return {
|
||||
providers,
|
||||
};
|
||||
}
|
||||
|
||||
export default function SettingsSecurity({ loaderData }: Route.ComponentProps) {
|
||||
const { providers } = loaderData;
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { user } = useSession();
|
||||
|
||||
const hasEmailPasswordAccount = providers.includes('DOCUMENSO');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title={_(msg`Security`)}
|
||||
subtitle={_(msg`Here you can manage your password and security settings.`)}
|
||||
/>
|
||||
|
||||
{user.identityProvider === 'DOCUMENSO' && (
|
||||
{hasEmailPasswordAccount && (
|
||||
<>
|
||||
<PasswordForm user={user} />
|
||||
|
||||
@ -46,7 +72,7 @@ export default function SettingsSecurity() {
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="mr-4">
|
||||
{user.identityProvider === 'DOCUMENSO' ? (
|
||||
{hasEmailPasswordAccount ? (
|
||||
<Trans>
|
||||
Add an authenticator to serve as a secondary authentication method when signing in,
|
||||
or when signing documents.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { CreatePasskeyDialog } from '~/components/dialogs/create-passkey-dialog';
|
||||
import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { SettingsSecurityPasskeyTable } from '~/components/tables/settings-security-passkey-table';
|
||||
|
||||
@ -21,7 +21,7 @@ export default function SettingsPasskeys() {
|
||||
subtitle={_(msg`Manage your passkeys.`)}
|
||||
hideDivider={true}
|
||||
>
|
||||
<CreatePasskeyDialog />
|
||||
<PasskeyCreateDialog />
|
||||
</SettingsHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
|
||||
@ -59,10 +59,10 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
}
|
||||
|
||||
return superLoaderJson({
|
||||
isAccessAuthValid: true as const,
|
||||
isAccessAuthValid: true,
|
||||
template,
|
||||
directTemplateRecipient,
|
||||
});
|
||||
} as const);
|
||||
}
|
||||
|
||||
export default function DirectTemplatePage() {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "sh .bin/build.sh",
|
||||
"build:app": "cross-env NODE_ENV=production react-router build",
|
||||
"build:app": "npm run typecheck && cross-env NODE_ENV=production react-router build",
|
||||
"build:server": "cross-env NODE_ENV=production rollup -c rollup.config.mjs",
|
||||
"dev": "npm run with:env -- react-router dev",
|
||||
"start": "npm run with:env -- cross-env NODE_ENV=production node build/server/main.js",
|
||||
@ -26,7 +26,6 @@
|
||||
"@epic-web/remember": "^1.1.0",
|
||||
"@hono/node-server": "^1.13.7",
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
"@hono/zod-validator": "^0.4.2",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@lingui/core": "^5.2.0",
|
||||
"@lingui/detect-locale": "^5.2.0",
|
||||
@ -41,7 +40,7 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"colord": "^2.9.3",
|
||||
"framer-motion": "^10.12.8",
|
||||
"hono": "4.6.15",
|
||||
"hono": "4.7.0",
|
||||
"hono-react-router-adapter": "^0.6.2",
|
||||
"input-otp": "^1.2.4",
|
||||
"isbot": "^5.1.17",
|
||||
@ -98,6 +97,7 @@
|
||||
"typescript": "5.6.2",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import linguiMacro from '@lingui/babel-plugin-lingui-macro';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
@ -21,7 +22,7 @@ const config = {
|
||||
external: [/node_modules/],
|
||||
plugins: [
|
||||
typescript({
|
||||
// noEmitOnError: true,
|
||||
noEmitOnError: true,
|
||||
moduleResolution: 'bundler',
|
||||
include: ['server/**/*', '../../packages/**/*', '../../packages/lib/translations/**/*'],
|
||||
}),
|
||||
@ -33,6 +34,7 @@ const config = {
|
||||
'@documenso/auth/*',
|
||||
'@documenso/lib/*',
|
||||
'@documenso/trpc/*',
|
||||
'@documenso/email/*',
|
||||
],
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
|
||||
}),
|
||||
@ -41,7 +43,7 @@ const config = {
|
||||
babelHelpers: 'bundled',
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
presets: ['@babel/preset-typescript'],
|
||||
plugins: ['@lingui/babel-plugin-lingui-macro'],
|
||||
plugins: [linguiMacro],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
110
apps/remix/server/api/files.ts
Normal file
110
apps/remix/server/api/files.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { putFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import {
|
||||
getPresignGetUrl,
|
||||
getPresignPostUrl,
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
|
||||
import type { HonoEnv } from '../router';
|
||||
import {
|
||||
type TGetPresignedGetUrlResponse,
|
||||
type TGetPresignedPostUrlResponse,
|
||||
ZGetPresignedGetUrlRequestSchema,
|
||||
ZGetPresignedPostUrlRequestSchema,
|
||||
ZUploadPdfRequestSchema,
|
||||
} from './files.types';
|
||||
|
||||
export const filesRoute = new Hono<HonoEnv>()
|
||||
/**
|
||||
* Uploads a document file to the appropriate storage location and creates
|
||||
* a document data record.
|
||||
*/
|
||||
.post('/upload-pdf', sValidator('form', ZUploadPdfRequestSchema), async (c) => {
|
||||
try {
|
||||
const { file } = c.req.valid('form');
|
||||
|
||||
if (!file) {
|
||||
return c.json({ error: 'No file provided' }, 400);
|
||||
}
|
||||
|
||||
// Todo: Do we want to validate the file type?
|
||||
// if (file.type !== 'application/pdf') {
|
||||
// return c.json({ error: 'File must be a PDF' }, 400);
|
||||
// }
|
||||
|
||||
// Todo: This is new.
|
||||
// Add file size validation.
|
||||
// Convert MB to bytes (1 MB = 1024 * 1024 bytes)
|
||||
const MAX_FILE_SIZE = APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024;
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return c.json({ error: 'File too large' }, 400);
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
|
||||
console.error(`PDF upload parse error: ${e.message}`);
|
||||
|
||||
throw new AppError('INVALID_DOCUMENT_FILE');
|
||||
});
|
||||
|
||||
if (pdf.isEncrypted) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE');
|
||||
}
|
||||
|
||||
// Todo: Test this.
|
||||
if (!file.name.endsWith('.pdf')) {
|
||||
Object.defineProperty(file, 'name', {
|
||||
writable: true,
|
||||
value: `${file.name}.pdf`,
|
||||
});
|
||||
}
|
||||
|
||||
const { type, data } = await putFileServerSide(file);
|
||||
|
||||
const result = await createDocumentData({ type, data });
|
||||
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
return c.json({ error: 'Upload failed' }, 500);
|
||||
}
|
||||
})
|
||||
.post('/presigned-get-url', sValidator('json', ZGetPresignedGetUrlRequestSchema), async (c) => {
|
||||
const { key } = await c.req.json();
|
||||
console.log(key);
|
||||
|
||||
try {
|
||||
const { url } = await getPresignGetUrl(key || '');
|
||||
|
||||
return c.json({ url } satisfies TGetPresignedGetUrlResponse);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR);
|
||||
}
|
||||
})
|
||||
.post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => {
|
||||
const { fileName, contentType } = c.req.valid('json');
|
||||
|
||||
try {
|
||||
console.log({
|
||||
fileName,
|
||||
});
|
||||
const { key, url } = await getPresignPostUrl(fileName, contentType);
|
||||
console.log(key);
|
||||
|
||||
return c.json({ key, url } satisfies TGetPresignedPostUrlResponse);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR);
|
||||
}
|
||||
});
|
||||
38
apps/remix/server/api/files.types.ts
Normal file
38
apps/remix/server/api/files.types.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
|
||||
export const ZUploadPdfRequestSchema = z.object({
|
||||
file: z.instanceof(File),
|
||||
});
|
||||
|
||||
export const ZUploadPdfResponseSchema = DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
});
|
||||
|
||||
export type TUploadPdfRequest = z.infer<typeof ZUploadPdfRequestSchema>;
|
||||
export type TUploadPdfResponse = z.infer<typeof ZUploadPdfResponseSchema>;
|
||||
|
||||
export const ZGetPresignedPostUrlRequestSchema = z.object({
|
||||
fileName: z.string().min(1),
|
||||
contentType: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZGetPresignedPostUrlResponseSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZGetPresignedGetUrlRequestSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZGetPresignedGetUrlResponseSchema = z.object({
|
||||
url: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TGetPresignedPostUrlRequest = z.infer<typeof ZGetPresignedPostUrlRequestSchema>;
|
||||
export type TGetPresignedPostUrlResponse = z.infer<typeof ZGetPresignedPostUrlResponseSchema>;
|
||||
export type TGetPresignedGetUrlRequest = z.infer<typeof ZGetPresignedGetUrlRequestSchema>;
|
||||
export type TGetPresignedGetUrlResponse = z.infer<typeof ZGetPresignedGetUrlResponseSchema>;
|
||||
@ -1,12 +1,20 @@
|
||||
import type { Context, Next } from 'hono';
|
||||
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
|
||||
import { getCookie } from 'hono/cookie';
|
||||
|
||||
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
||||
import { setCsrfCookie } from '@documenso/auth/server/lib/session/session-cookies';
|
||||
import { AppLogger } from '@documenso/lib/utils/debugger';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
const logger = new AppLogger('Middleware');
|
||||
|
||||
/**
|
||||
* Middleware for initial page loads.
|
||||
*
|
||||
* You won't be able to easily handle sequential page loads because they will be
|
||||
* called under `path.data`
|
||||
*
|
||||
* Example an initial page load would be `/documents` then if the user click templates
|
||||
* the path here would be `/templates.data`.
|
||||
*/
|
||||
export const appMiddleware = async (c: Context, next: Next) => {
|
||||
const { req } = c;
|
||||
const { path } = req;
|
||||
@ -24,70 +32,64 @@ export const appMiddleware = async (c: Context, next: Next) => {
|
||||
const referrerUrl = referrer ? new URL(referrer) : null;
|
||||
const referrerPathname = referrerUrl ? referrerUrl.pathname : null;
|
||||
|
||||
// Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
|
||||
const resetPreferredTeamUrl =
|
||||
referrerPathname &&
|
||||
referrerPathname.startsWith('/t/') &&
|
||||
(!path.startsWith('/t/') || path === '/');
|
||||
// Set csrf token if not set.
|
||||
const csrfToken = getCookie(c, 'csrfToken');
|
||||
|
||||
// Redirect root page to `/documents` or `/t/{preferredTeamUrl}/documents`.
|
||||
if (path === '/') {
|
||||
logger.log('Redirecting from root to documents');
|
||||
|
||||
const redirectUrlPath = formatDocumentsPath(
|
||||
resetPreferredTeamUrl ? undefined : preferredTeamUrl,
|
||||
);
|
||||
|
||||
const redirectUrl = new URL(redirectUrlPath, req.url);
|
||||
|
||||
return c.redirect(redirectUrl);
|
||||
// Todo: Currently not working.
|
||||
if (!csrfToken) {
|
||||
await setCsrfCookie(c);
|
||||
}
|
||||
|
||||
// Redirect `/t` to `/settings/teams`.
|
||||
if (path === '/t' || path === '/t/') {
|
||||
logger.log('Redirecting to /settings/teams');
|
||||
// // Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
|
||||
// const resetPreferredTeamUrl =
|
||||
// referrerPathname &&
|
||||
// referrerPathname.startsWith('/t/') &&
|
||||
// (!path.startsWith('/t/') || path === '/');
|
||||
|
||||
const redirectUrl = new URL('/settings/teams', req.url);
|
||||
return c.redirect(redirectUrl);
|
||||
}
|
||||
// // Redirect root page to `/documents` or `/t/{preferredTeamUrl}/documents`.
|
||||
// if (path === '/') {
|
||||
// logger.log('Redirecting from root to documents');
|
||||
|
||||
// Redirect `/t/<team_url>` to `/t/<team_url>/documents`.
|
||||
if (TEAM_URL_ROOT_REGEX.test(path)) {
|
||||
logger.log('Redirecting team documents');
|
||||
// const redirectUrlPath = formatDocumentsPath(
|
||||
// resetPreferredTeamUrl ? undefined : preferredTeamUrl,
|
||||
// );
|
||||
|
||||
const redirectUrl = new URL(`${path}/documents`, req.url);
|
||||
setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
|
||||
// const redirectUrl = new URL(redirectUrlPath, req.url);
|
||||
|
||||
return c.redirect(redirectUrl);
|
||||
}
|
||||
// return c.redirect(redirectUrl);
|
||||
// }
|
||||
|
||||
// Set the preferred team url cookie if user accesses a team page.
|
||||
if (path.startsWith('/t/')) {
|
||||
setCookie(c, 'preferred-team-url', path.split('/')[2]);
|
||||
return next();
|
||||
}
|
||||
// // Redirect `/t` to `/settings/teams`.
|
||||
// if (path === '/t' || path === '/t/') {
|
||||
// logger.log('Redirecting to /settings/teams');
|
||||
|
||||
// Clear preferred team url cookie if user accesses a non team page from a team page.
|
||||
if (resetPreferredTeamUrl || path === '/documents') {
|
||||
logger.log('Resetting preferred team url');
|
||||
// const redirectUrl = new URL('/settings/teams', req.url);
|
||||
// return c.redirect(redirectUrl);
|
||||
// }
|
||||
|
||||
deleteCookie(c, 'preferred-team-url');
|
||||
return next();
|
||||
}
|
||||
// // Redirect `/t/<team_url>` to `/t/<team_url>/documents`.
|
||||
// if (TEAM_URL_ROOT_REGEX.test(path)) {
|
||||
// logger.log('Redirecting team documents');
|
||||
|
||||
// Todo: Test
|
||||
if (path.startsWith('/embed')) {
|
||||
const origin = req.header('Origin') ?? '*';
|
||||
// const redirectUrl = new URL(`${path}/documents`, req.url);
|
||||
// setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
|
||||
|
||||
// Allow third parties to iframe the document.
|
||||
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
c.header('Access-Control-Allow-Origin', origin);
|
||||
c.header('Content-Security-Policy', `frame-ancestors ${origin}`);
|
||||
c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
c.header('X-Content-Type-Options', 'nosniff');
|
||||
// return c.redirect(redirectUrl);
|
||||
// }
|
||||
|
||||
return next();
|
||||
}
|
||||
// // Set the preferred team url cookie if user accesses a team page.
|
||||
// if (path.startsWith('/t/')) {
|
||||
// setCookie(c, 'preferred-team-url', path.split('/')[2]);
|
||||
// return next();
|
||||
// }
|
||||
|
||||
// // Clear preferred team url cookie if user accesses a non team page from a team page.
|
||||
// if (resetPreferredTeamUrl || path === '/documents') {
|
||||
// logger.log('Resetting preferred team url');
|
||||
|
||||
// deleteCookie(c, 'preferred-team-url');
|
||||
// return next();
|
||||
// }
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import { Hono } from 'hono';
|
||||
import { contextStorage } from 'hono/context-storage';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { tsRestHonoApp } from '@documenso/api/hono';
|
||||
import { auth } from '@documenso/auth/server';
|
||||
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
||||
|
||||
import { filesRoute } from './api/files';
|
||||
import { type AppContext, appContext } from './context';
|
||||
import { appMiddleware } from './middleware';
|
||||
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
||||
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
|
||||
|
||||
@ -30,12 +27,17 @@ const app = new Hono<HonoEnv>();
|
||||
app.use(contextStorage());
|
||||
app.use(appContext);
|
||||
|
||||
// App middleware.
|
||||
// app.use('*', appMiddleware);
|
||||
/**
|
||||
* Middleware for initial page loads.
|
||||
*/
|
||||
app.use('*', appMiddleware);
|
||||
|
||||
// Auth server.
|
||||
app.route('/api/auth', auth);
|
||||
|
||||
// Files route.
|
||||
app.route('/api/files', filesRoute);
|
||||
|
||||
// API servers. Todo: Configure max durations, etc?
|
||||
app.route('/api/v1', tsRestHonoApp);
|
||||
app.use('/api/jobs/*', jobsClient.getApiHandler());
|
||||
@ -45,73 +47,4 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
|
||||
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
||||
app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c)); // Todo: Add next()?
|
||||
|
||||
// Temp uploader.
|
||||
app
|
||||
.post('/api/file', async (c) => {
|
||||
try {
|
||||
const formData = await c.req.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
return c.json({ error: 'No file provided' }, 400);
|
||||
}
|
||||
|
||||
// Add file size validation
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return c.json({ error: 'File too large' }, 400);
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
|
||||
console.error(`PDF upload parse error: ${e.message}`);
|
||||
|
||||
throw new AppError('INVALID_DOCUMENT_FILE');
|
||||
});
|
||||
|
||||
if (pdf.isEncrypted) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE');
|
||||
}
|
||||
|
||||
// Todo: Test this.
|
||||
if (!file.name.endsWith('.pdf')) {
|
||||
Object.defineProperty(file, 'name', {
|
||||
writable: true,
|
||||
value: `${file.name}.pdf`,
|
||||
});
|
||||
}
|
||||
|
||||
const { type, data } = await putFile(file);
|
||||
|
||||
const result = await createDocumentData({ type, data });
|
||||
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
return c.json({ error: 'Upload failed' }, 500);
|
||||
}
|
||||
})
|
||||
.get('/api/file', async (c) => {
|
||||
const key = c.req.query('key');
|
||||
|
||||
const { url } = await getPresignGetUrl(key || '');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get file "${key}", failed with status code ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
const binaryData = new Uint8Array(buffer);
|
||||
|
||||
return c.json({
|
||||
binaryData,
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"moduleDetection": "force",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user