mirror of
https://github.com/documenso/documenso.git
synced 2025-11-26 22:44:41 +10:00
Compare commits
5 Commits
v1.13.0
...
cb9bf407f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb9bf407f7 | ||
|
|
4a3859ec60 | ||
|
|
49b792503f | ||
|
|
c3dc76b1b4 | ||
|
|
daab8461c7 |
@@ -13,6 +13,10 @@ NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
|||||||
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
|
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
|
||||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
||||||
|
# Find documentation on setting up Microsoft OAuth here:
|
||||||
|
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#microsoft-oauth-azure-ad
|
||||||
|
NEXT_PRIVATE_MICROSOFT_CLIENT_ID=""
|
||||||
|
NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET=""
|
||||||
|
|
||||||
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
|
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
|
||||||
NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
||||||
|
|||||||
@@ -27,3 +27,33 @@ NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=<your-client-secret>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Finally verify the signing in with Google works by signing in with your Google account and checking the email address in your profile.
|
Finally verify the signing in with Google works by signing in with your Google account and checking the email address in your profile.
|
||||||
|
|
||||||
|
## Microsoft OAuth (Azure AD)
|
||||||
|
|
||||||
|
To use Microsoft OAuth, you will need to create an Azure AD application registration in the Microsoft Azure portal. This will allow users to sign in with their Microsoft accounts.
|
||||||
|
|
||||||
|
### Create and configure a new Azure AD application
|
||||||
|
|
||||||
|
1. Go to the [Azure Portal](https://portal.azure.com/)
|
||||||
|
2. Navigate to **Azure Active Directory** (or **Microsoft Entra ID** in newer Azure portals)
|
||||||
|
3. In the left sidebar, click **App registrations**
|
||||||
|
4. Click **New registration**
|
||||||
|
5. Enter a name for your application (e.g., "Documenso")
|
||||||
|
6. Under **Supported account types**, select **Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)** to allow any Microsoft account to sign in
|
||||||
|
7. Under **Redirect URI**, select **Web** and enter: `https://<documenso-domain>/api/auth/callback/microsoft`
|
||||||
|
8. Click **Register**
|
||||||
|
|
||||||
|
### Configure the application
|
||||||
|
|
||||||
|
1. After registration, you'll be taken to the app's overview page
|
||||||
|
2. Copy the **Application (client) ID** - this will be your `NEXT_PRIVATE_MICROSOFT_CLIENT_ID`
|
||||||
|
3. In the left sidebar, click **Certificates & secrets**
|
||||||
|
4. Under **Client secrets**, click **New client secret**
|
||||||
|
5. Add a description and select an expiration period
|
||||||
|
6. Click **Add** and copy the **Value** (not the Secret ID) - this will be your `NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET`
|
||||||
|
7. In the Documenso environment variables, set the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEXT_PRIVATE_MICROSOFT_CLIENT_ID=<your-application-client-id>
|
||||||
|
NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET=<your-client-secret-value>
|
||||||
|
```
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const DocumentMoveToFolderDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFolders.useQuery(
|
const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery(
|
||||||
{
|
{
|
||||||
parentId: currentFolderId,
|
parentId: currentFolderId,
|
||||||
type: FolderType.DOCUMENT,
|
type: FolderType.DOCUMENT,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
|
|||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteFolder({
|
await deleteFolder({
|
||||||
id: folder.id,
|
folderId: folder.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const FolderMoveDialog = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation();
|
const { mutateAsync: moveFolder } = trpc.folder.updateFolder.useMutation();
|
||||||
|
|
||||||
const form = useForm<TMoveFolderFormSchema>({
|
const form = useForm<TMoveFolderFormSchema>({
|
||||||
resolver: zodResolver(ZMoveFolderFormSchema),
|
resolver: zodResolver(ZMoveFolderFormSchema),
|
||||||
@@ -63,12 +63,16 @@ export const FolderMoveDialog = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({ targetFolderId }: TMoveFolderFormSchema) => {
|
const onFormSubmit = async ({ targetFolderId }: TMoveFolderFormSchema) => {
|
||||||
if (!folder) return;
|
if (!folder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await moveFolder({
|
await moveFolder({
|
||||||
id: folder.id,
|
folderId: folder.id,
|
||||||
|
data: {
|
||||||
parentId: targetFolderId || null,
|
parentId: targetFolderId || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
|
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
|
||||||
|
|
||||||
const isTeamContext = !!team;
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof ZUpdateFolderFormSchema>>({
|
const form = useForm<z.infer<typeof ZUpdateFolderFormSchema>>({
|
||||||
resolver: zodResolver(ZUpdateFolderFormSchema),
|
resolver: zodResolver(ZUpdateFolderFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -87,11 +85,11 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateFolder({
|
await updateFolder({
|
||||||
id: folder.id,
|
folderId: folder.id,
|
||||||
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
visibility: isTeamContext
|
visibility: data.visibility,
|
||||||
? (data.visibility ?? DocumentVisibility.EVERYONE)
|
},
|
||||||
: DocumentVisibility.EVERYONE,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -140,7 +138,6 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isTeamContext && (
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="visibility"
|
name="visibility"
|
||||||
@@ -171,7 +168,6 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function TemplateMoveToFolderDialog({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFolders.useQuery(
|
const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery(
|
||||||
{
|
{
|
||||||
parentId: currentFolderId ?? null,
|
parentId: currentFolderId ?? null,
|
||||||
type: FolderType.TEMPLATE,
|
type: FolderType.TEMPLATE,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export type SignInFormProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
|
isMicrosoftSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
oidcProviderLabel?: string;
|
oidcProviderLabel?: string;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
@@ -79,6 +80,7 @@ export const SignInForm = ({
|
|||||||
className,
|
className,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
oidcProviderLabel,
|
oidcProviderLabel,
|
||||||
returnTo,
|
returnTo,
|
||||||
@@ -95,6 +97,8 @@ export const SignInForm = ({
|
|||||||
'totp' | 'backup'
|
'totp' | 'backup'
|
||||||
>('totp');
|
>('totp');
|
||||||
|
|
||||||
|
const hasSocialAuthEnabled = isGoogleSSOEnabled || isMicrosoftSSOEnabled || isOIDCSSOEnabled;
|
||||||
|
|
||||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||||
|
|
||||||
const redirectPath = useMemo(() => {
|
const redirectPath = useMemo(() => {
|
||||||
@@ -271,6 +275,22 @@ export const SignInForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSignInWithMicrosoftClick = async () => {
|
||||||
|
try {
|
||||||
|
await authClient.microsoft.signIn({
|
||||||
|
redirectPath,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`An unknown error occurred`),
|
||||||
|
description: _(
|
||||||
|
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSignInWithOIDCClick = async () => {
|
const onSignInWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await authClient.oidc.signIn({
|
await authClient.oidc.signIn({
|
||||||
@@ -363,7 +383,7 @@ export const SignInForm = ({
|
|||||||
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
|
{hasSocialAuthEnabled && (
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="bg-border h-px flex-1" />
|
||||||
<span className="text-muted-foreground bg-transparent">
|
<span className="text-muted-foreground bg-transparent">
|
||||||
@@ -387,6 +407,20 @@ export const SignInForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isMicrosoftSSOEnabled && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="bg-background text-muted-foreground border"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onSignInWithMicrosoftClick}
|
||||||
|
>
|
||||||
|
<img className="mr-2 h-4 w-4" alt="Microsoft Logo" src={'/static/microsoft.svg'} />
|
||||||
|
Microsoft
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isOIDCSSOEnabled && (
|
{isOIDCSSOEnabled && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type SignUpFormProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
|
isMicrosoftSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ export const SignUpForm = ({
|
|||||||
className,
|
className,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
}: SignUpFormProps) => {
|
}: SignUpFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@@ -84,6 +86,8 @@ export const SignUpForm = ({
|
|||||||
|
|
||||||
const utmSrc = searchParams.get('utm_source') ?? null;
|
const utmSrc = searchParams.get('utm_source') ?? null;
|
||||||
|
|
||||||
|
const hasSocialAuthEnabled = isGoogleSSOEnabled || isMicrosoftSSOEnabled || isOIDCSSOEnabled;
|
||||||
|
|
||||||
const form = useForm<TSignUpFormSchema>({
|
const form = useForm<TSignUpFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -148,6 +152,20 @@ export const SignUpForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSignUpWithMicrosoftClick = async () => {
|
||||||
|
try {
|
||||||
|
await authClient.microsoft.signIn();
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`An unknown error occurred`),
|
||||||
|
description: _(
|
||||||
|
msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`,
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSignUpWithOIDCClick = async () => {
|
const onSignUpWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await authClient.oidc.signIn();
|
await authClient.oidc.signIn();
|
||||||
@@ -227,7 +245,7 @@ export const SignUpForm = ({
|
|||||||
<fieldset
|
<fieldset
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-[550px] w-full flex-col gap-y-4',
|
'flex h-[550px] w-full flex-col gap-y-4',
|
||||||
(isGoogleSSOEnabled || isOIDCSSOEnabled) && 'h-[650px]',
|
hasSocialAuthEnabled && 'h-[650px]',
|
||||||
)}
|
)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -302,7 +320,7 @@ export const SignUpForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
|
{hasSocialAuthEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="bg-border h-px flex-1" />
|
||||||
@@ -330,6 +348,26 @@ export const SignUpForm = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isMicrosoftSSOEnabled && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
variant={'outline'}
|
||||||
|
className="bg-background text-muted-foreground border"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onSignUpWithMicrosoftClick}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="mr-2 h-4 w-4"
|
||||||
|
alt="Microsoft Logo"
|
||||||
|
src={'/static/microsoft.svg'}
|
||||||
|
/>
|
||||||
|
<Trans>Sign Up with Microsoft</Trans>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{isOIDCSSOEnabled && (
|
{isOIDCSSOEnabled && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@@ -28,22 +29,15 @@ import { useCurrentTeam } from '~/providers/team';
|
|||||||
export type FolderCardProps = {
|
export type FolderCardProps = {
|
||||||
folder: TFolderWithSubfolders;
|
folder: TFolderWithSubfolders;
|
||||||
onMove: (folder: TFolderWithSubfolders) => void;
|
onMove: (folder: TFolderWithSubfolders) => void;
|
||||||
onPin: (folderId: string) => void;
|
|
||||||
onUnpin: (folderId: string) => void;
|
|
||||||
onSettings: (folder: TFolderWithSubfolders) => void;
|
onSettings: (folder: TFolderWithSubfolders) => void;
|
||||||
onDelete: (folder: TFolderWithSubfolders) => void;
|
onDelete: (folder: TFolderWithSubfolders) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FolderCard = ({
|
export const FolderCard = ({ folder, onMove, onSettings, onDelete }: FolderCardProps) => {
|
||||||
folder,
|
|
||||||
onMove,
|
|
||||||
onPin,
|
|
||||||
onUnpin,
|
|
||||||
onSettings,
|
|
||||||
onDelete,
|
|
||||||
}: FolderCardProps) => {
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
|
const { mutateAsync: updateFolderMutation } = trpc.folder.updateFolder.useMutation();
|
||||||
|
|
||||||
const formatPath = () => {
|
const formatPath = () => {
|
||||||
const rootPath =
|
const rootPath =
|
||||||
folder.type === FolderType.DOCUMENT
|
folder.type === FolderType.DOCUMENT
|
||||||
@@ -53,6 +47,15 @@ export const FolderCard = ({
|
|||||||
return `${rootPath}/f/${folder.id}`;
|
return `${rootPath}/f/${folder.id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateFolder = async ({ pinned }: { pinned: boolean }) => {
|
||||||
|
await updateFolderMutation({
|
||||||
|
folderId: folder.id,
|
||||||
|
data: {
|
||||||
|
pinned,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={formatPath()} data-folder-id={folder.id} data-folder-name={folder.name}>
|
<Link to={formatPath()} data-folder-id={folder.id} data-folder-name={folder.name}>
|
||||||
<Card className="hover:bg-muted/50 border-border h-full border transition-all">
|
<Card className="hover:bg-muted/50 border-border h-full border transition-all">
|
||||||
@@ -112,9 +115,7 @@ export const FolderCard = ({
|
|||||||
<Trans>Move</Trans>
|
<Trans>Move</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={async () => updateFolder({ pinned: !folder.pinned })}>
|
||||||
onClick={() => (folder.pinned ? onUnpin(folder.id) : onPin(folder.id))}
|
|
||||||
>
|
|
||||||
<PinIcon className="mr-2 h-4 w-4" />
|
<PinIcon className="mr-2 h-4 w-4" />
|
||||||
{folder.pinned ? <Trans>Unpin</Trans> : <Trans>Pin</Trans>}
|
{folder.pinned ? <Trans>Unpin</Trans> : <Trans>Pin</Trans>}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
|
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
|
||||||
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
|
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
|
||||||
|
|
||||||
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
|
|
||||||
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
|
|
||||||
|
|
||||||
const { data: foldersData, isPending } = trpc.folder.getFolders.useQuery({
|
const { data: foldersData, isPending } = trpc.folder.getFolders.useQuery({
|
||||||
type,
|
type,
|
||||||
parentId,
|
parentId,
|
||||||
@@ -155,8 +152,6 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
@@ -180,8 +175,6 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ export default function DocumentsFoldersPage() {
|
|||||||
parentId: null,
|
parentId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
|
|
||||||
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
|
|
||||||
|
|
||||||
const navigateToFolder = (folderId?: string | null) => {
|
const navigateToFolder = (folderId?: string | null) => {
|
||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
|
|
||||||
@@ -113,8 +110,6 @@ export default function DocumentsFoldersPage() {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
@@ -147,8 +142,6 @@ export default function DocumentsFoldersPage() {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ export default function TemplatesFoldersPage() {
|
|||||||
parentId: null,
|
parentId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
|
|
||||||
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
|
|
||||||
|
|
||||||
const navigateToFolder = (folderId?: string | null) => {
|
const navigateToFolder = (folderId?: string | null) => {
|
||||||
const templatesPath = formatTemplatesPath(team.url);
|
const templatesPath = formatTemplatesPath(team.url);
|
||||||
|
|
||||||
@@ -113,8 +110,6 @@ export default function TemplatesFoldersPage() {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
@@ -147,8 +142,6 @@ export default function TemplatesFoldersPage() {
|
|||||||
setFolderToMove(folder);
|
setFolderToMove(folder);
|
||||||
setIsMovingFolder(true);
|
setIsMovingFolder(true);
|
||||||
}}
|
}}
|
||||||
onPin={(folderId) => void pinFolder({ folderId })}
|
|
||||||
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
||||||
onSettings={(folder) => {
|
onSettings={(folder) => {
|
||||||
setFolderToSettings(folder);
|
setFolderToSettings(folder);
|
||||||
setIsSettingsFolderOpen(true);
|
setIsSettingsFolderOpen(true);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Link, redirect } from 'react-router';
|
|||||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import {
|
import {
|
||||||
IS_GOOGLE_SSO_ENABLED,
|
IS_GOOGLE_SSO_ENABLED,
|
||||||
|
IS_MICROSOFT_SSO_ENABLED,
|
||||||
IS_OIDC_SSO_ENABLED,
|
IS_OIDC_SSO_ENABLED,
|
||||||
OIDC_PROVIDER_LABEL,
|
OIDC_PROVIDER_LABEL,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
@@ -23,6 +24,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
// SSR env variables.
|
// SSR env variables.
|
||||||
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
||||||
|
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
|
||||||
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
||||||
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
||||||
|
|
||||||
@@ -32,13 +34,15 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
oidcProviderLabel,
|
oidcProviderLabel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SignIn({ loaderData }: Route.ComponentProps) {
|
export default function SignIn({ loaderData }: Route.ComponentProps) {
|
||||||
const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData;
|
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } =
|
||||||
|
loaderData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
@@ -54,6 +58,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
|
|||||||
|
|
||||||
<SignInForm
|
<SignInForm
|
||||||
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||||
|
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
oidcProviderLabel={oidcProviderLabel}
|
oidcProviderLabel={oidcProviderLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { redirect } from 'react-router';
|
import { redirect } from 'react-router';
|
||||||
|
|
||||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
import {
|
||||||
|
IS_GOOGLE_SSO_ENABLED,
|
||||||
|
IS_MICROSOFT_SSO_ENABLED,
|
||||||
|
IS_OIDC_SSO_ENABLED,
|
||||||
|
} from '@documenso/lib/constants/auth';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
@@ -17,6 +21,7 @@ export function loader() {
|
|||||||
|
|
||||||
// SSR env variables.
|
// SSR env variables.
|
||||||
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
||||||
|
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
|
||||||
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
||||||
|
|
||||||
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
|
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
|
||||||
@@ -25,17 +30,19 @@ export function loader() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SignUp({ loaderData }: Route.ComponentProps) {
|
export default function SignUp({ loaderData }: Route.ComponentProps) {
|
||||||
const { isGoogleSSOEnabled, isOIDCSSOEnabled } = loaderData;
|
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled } = loaderData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignUpForm
|
<SignUpForm
|
||||||
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
|
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
|
||||||
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||||
|
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
1
apps/remix/public/static/microsoft.svg
Normal file
1
apps/remix/public/static/microsoft.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" width="256" height="256" preserveAspectRatio="xMidYMid"><path fill="#F1511B" d="M121.666 121.666H0V0h121.666z"/><path fill="#80CC28" d="M256 121.666H134.335V0H256z"/><path fill="#00ADEF" d="M121.663 256.002H0V134.336h121.663z"/><path fill="#FBBC09" d="M256 256.002H134.335V134.336H256z"/></svg>
|
||||||
|
After Width: | Height: | Size: 356 B |
13
package-lock.json
generated
13
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
|
"pdf2json": "^4.0.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
@@ -27198,6 +27199,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf2json": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf2json/-/pdf2json-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-WkezNsLK8sGpuFC7+PPP0DsXROwdoOxmXPBTtUWWkCwCi/Vi97MRC52Ly6FWIJjOKIywpm/L2oaUgSrmtU+7ZQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"pdf2json": "bin/pdf2json.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pdfjs-dist": {
|
"node_modules/pdfjs-dist": {
|
||||||
"version": "3.11.174",
|
"version": "3.11.174",
|
||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
|
"pdf2json": "^4.0.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
seedDraftDocument,
|
seedDraftDocument,
|
||||||
seedPendingDocument,
|
seedPendingDocument,
|
||||||
} from '@documenso/prisma/seed/documents';
|
} from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
@@ -326,11 +327,6 @@ test.describe('Document API V2', () => {
|
|||||||
data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) },
|
data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const asdf = await res.json();
|
|
||||||
console.log({
|
|
||||||
asdf,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.ok()).toBeTruthy();
|
expect(res.ok()).toBeTruthy();
|
||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
});
|
});
|
||||||
@@ -407,11 +403,6 @@ test.describe('Document API V2', () => {
|
|||||||
headers: { Authorization: `Bearer ${tokenA}` },
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
const asdf = await res.json();
|
|
||||||
console.log({
|
|
||||||
asdf,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.ok()).toBeTruthy();
|
expect(res.ok()).toBeTruthy();
|
||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
});
|
});
|
||||||
@@ -2715,4 +2706,154 @@ test.describe('Document API V2', () => {
|
|||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Folder list endpoint', () => {
|
||||||
|
test('should block unauthorized access to folder list endpoint', async ({ request }) => {
|
||||||
|
await seedBlankFolder(userA, teamA.id);
|
||||||
|
await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/folder`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const { data } = await res.json();
|
||||||
|
expect(data.every((folder: { userId: number }) => folder.userId !== userA.id)).toBe(true);
|
||||||
|
expect(data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to folder list endpoint', async ({ request }) => {
|
||||||
|
await seedBlankFolder(userA, teamA.id);
|
||||||
|
await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
// Other team folders should not be visible.
|
||||||
|
await seedBlankFolder(userA, teamB.id);
|
||||||
|
await seedBlankFolder(userA, teamB.id);
|
||||||
|
|
||||||
|
// Other team and user folders should not be visible.
|
||||||
|
await seedBlankFolder(userB, teamB.id);
|
||||||
|
await seedBlankFolder(userB, teamB.id);
|
||||||
|
|
||||||
|
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/folder`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const { data } = await res.json();
|
||||||
|
|
||||||
|
expect(data.length).toBe(2);
|
||||||
|
expect(data.every((folder: { userId: number }) => folder.userId === userA.id)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Folder create endpoint', () => {
|
||||||
|
test('should block unauthorized access to folder create endpoint', async ({ request }) => {
|
||||||
|
const unauthorizedFolder = await seedBlankFolder(userB, teamB.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
parentId: unauthorizedFolder.id,
|
||||||
|
name: 'Test Folder',
|
||||||
|
type: 'DOCUMENT',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeFalsy();
|
||||||
|
expect(res.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to folder create endpoint', async ({ request }) => {
|
||||||
|
const authorizedFolder = await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
parentId: authorizedFolder.id,
|
||||||
|
name: 'Test Folder',
|
||||||
|
type: 'DOCUMENT',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const noParentRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
name: 'Test Folder',
|
||||||
|
type: 'DOCUMENT',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(noParentRes.ok()).toBeTruthy();
|
||||||
|
expect(noParentRes.status()).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Folder update endpoint', () => {
|
||||||
|
test('should block unauthorized access to folder update endpoint', async ({ request }) => {
|
||||||
|
const folder = await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/update`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
|
data: {
|
||||||
|
folderId: folder.id,
|
||||||
|
data: {
|
||||||
|
name: 'Updated Folder Name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeFalsy();
|
||||||
|
expect(res.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to folder update endpoint', async ({ request }) => {
|
||||||
|
const folder = await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/update`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
folderId: folder.id,
|
||||||
|
data: {
|
||||||
|
name: 'Updated Folder Name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Folder delete endpoint', () => {
|
||||||
|
test('should block unauthorized access to folder delete endpoint', async ({ request }) => {
|
||||||
|
const folder = await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/delete`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
|
data: { folderId: folder.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeFalsy();
|
||||||
|
expect(res.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to folder delete endpoint', async ({ request }) => {
|
||||||
|
const folder = await seedBlankFolder(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/folder/delete`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: { folderId: folder.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -222,6 +222,22 @@ export class AuthClient {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public microsoft = {
|
||||||
|
signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => {
|
||||||
|
const response = await this.client['oauth'].authorize.microsoft.$post({
|
||||||
|
json: { redirectPath },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.handleError(response);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.redirectUrl) {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public oidc = {
|
public oidc = {
|
||||||
signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => {
|
signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => {
|
||||||
const response = await this.client['oauth'].authorize.oidc.$post({ json: { redirectPath } });
|
const response = await this.client['oauth'].authorize.oidc.$post({ json: { redirectPath } });
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ export const GoogleAuthOptions: OAuthClientOptions = {
|
|||||||
bypassEmailVerification: false,
|
bypassEmailVerification: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MicrosoftAuthOptions: OAuthClientOptions = {
|
||||||
|
id: 'microsoft',
|
||||||
|
scope: ['openid', 'email', 'profile'],
|
||||||
|
clientId: env('NEXT_PRIVATE_MICROSOFT_CLIENT_ID') ?? '',
|
||||||
|
clientSecret: env('NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET') ?? '',
|
||||||
|
redirectUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/callback/microsoft`,
|
||||||
|
wellKnownUrl: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
|
||||||
|
bypassEmailVerification: false,
|
||||||
|
};
|
||||||
|
|
||||||
export const OidcAuthOptions: OAuthClientOptions = {
|
export const OidcAuthOptions: OAuthClientOptions = {
|
||||||
id: 'oidc',
|
id: 'oidc',
|
||||||
scope: ['openid', 'email', 'profile'],
|
scope: ['openid', 'email', 'profile'],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
|
|||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
|
||||||
import { GoogleAuthOptions, OidcAuthOptions } from '../config';
|
import { GoogleAuthOptions, MicrosoftAuthOptions, OidcAuthOptions } from '../config';
|
||||||
import { handleOAuthCallbackUrl } from '../lib/utils/handle-oauth-callback-url';
|
import { handleOAuthCallbackUrl } from '../lib/utils/handle-oauth-callback-url';
|
||||||
import { handleOAuthOrganisationCallbackUrl } from '../lib/utils/handle-oauth-organisation-callback-url';
|
import { handleOAuthOrganisationCallbackUrl } from '../lib/utils/handle-oauth-organisation-callback-url';
|
||||||
import type { HonoAuthContext } from '../types/context';
|
import type { HonoAuthContext } from '../types/context';
|
||||||
@@ -45,4 +45,11 @@ export const callbackRoute = new Hono<HonoAuthContext>()
|
|||||||
/**
|
/**
|
||||||
* Google callback verification.
|
* Google callback verification.
|
||||||
*/
|
*/
|
||||||
.get('/google', async (c) => handleOAuthCallbackUrl({ c, clientOptions: GoogleAuthOptions }));
|
.get('/google', async (c) => handleOAuthCallbackUrl({ c, clientOptions: GoogleAuthOptions }))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft callback verification.
|
||||||
|
*/
|
||||||
|
.get('/microsoft', async (c) =>
|
||||||
|
handleOAuthCallbackUrl({ c, clientOptions: MicrosoftAuthOptions }),
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { sValidator } from '@hono/standard-validator';
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { GoogleAuthOptions, OidcAuthOptions } from '../config';
|
import { GoogleAuthOptions, MicrosoftAuthOptions, OidcAuthOptions } from '../config';
|
||||||
import { handleOAuthAuthorizeUrl } from '../lib/utils/handle-oauth-authorize-url';
|
import { handleOAuthAuthorizeUrl } from '../lib/utils/handle-oauth-authorize-url';
|
||||||
import { getOrganisationAuthenticationPortalOptions } from '../lib/utils/organisation-portal';
|
import { getOrganisationAuthenticationPortalOptions } from '../lib/utils/organisation-portal';
|
||||||
import type { HonoAuthContext } from '../types/context';
|
import type { HonoAuthContext } from '../types/context';
|
||||||
@@ -24,6 +24,20 @@ export const oauthRoute = new Hono<HonoAuthContext>()
|
|||||||
redirectPath,
|
redirectPath,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft authorize endpoint.
|
||||||
|
*/
|
||||||
|
.post('/authorize/microsoft', sValidator('json', ZOAuthAuthorizeSchema), async (c) => {
|
||||||
|
const { redirectPath } = c.req.valid('json');
|
||||||
|
|
||||||
|
return handleOAuthAuthorizeUrl({
|
||||||
|
c,
|
||||||
|
clientOptions: MicrosoftAuthOptions,
|
||||||
|
redirectPath,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OIDC authorize endpoint.
|
* OIDC authorize endpoint.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const SALT_ROUNDS = 12;
|
|||||||
export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
|
export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
|
||||||
DOCUMENSO: 'Documenso',
|
DOCUMENSO: 'Documenso',
|
||||||
GOOGLE: 'Google',
|
GOOGLE: 'Google',
|
||||||
|
MICROSOFT: 'Microsoft',
|
||||||
OIDC: 'OIDC',
|
OIDC: 'OIDC',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +14,10 @@ export const IS_GOOGLE_SSO_ENABLED = Boolean(
|
|||||||
env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
|
env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IS_MICROSOFT_SSO_ENABLED = Boolean(
|
||||||
|
env('NEXT_PRIVATE_MICROSOFT_CLIENT_ID') && env('NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET'),
|
||||||
|
);
|
||||||
|
|
||||||
export const IS_OIDC_SSO_ENABLED = Boolean(
|
export const IS_OIDC_SSO_ENABLED = Boolean(
|
||||||
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
|
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
|
||||||
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
|
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
|
||||||
|
|||||||
@@ -81,11 +81,15 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
|||||||
const { user: owner } = envelope;
|
const { user: owner } = envelope;
|
||||||
|
|
||||||
const completedDocumentEmailAttachments = await Promise.all(
|
const completedDocumentEmailAttachments = await Promise.all(
|
||||||
envelope.envelopeItems.map(async (document) => {
|
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||||
const file = await getFileServerSide(document.documentData);
|
const file = await getFileServerSide(envelopeItem.documentData);
|
||||||
|
|
||||||
|
// Use the envelope title for version 1, and the envelope item title for version 2.
|
||||||
|
const fileNameToUse =
|
||||||
|
envelope.internalVersion === 1 ? envelope.title : envelopeItem.title + '.pdf';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : fileNameToUse + '.pdf',
|
||||||
content: Buffer.from(file),
|
content: Buffer.from(file),
|
||||||
contentType: 'application/pdf',
|
contentType: 'application/pdf',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
import type { TFolderType } from '../../types/folder-type';
|
||||||
import { FolderType } from '../../types/folder-type';
|
import { FolderType } from '../../types/folder-type';
|
||||||
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getTeamSettings } from '../team/get-team-settings';
|
import { getTeamSettings } from '../team/get-team-settings';
|
||||||
|
|
||||||
export interface CreateFolderOptions {
|
export interface CreateFolderOptions {
|
||||||
@@ -22,6 +24,27 @@ export const createFolder = async ({
|
|||||||
// This indirectly verifies whether the user has access to the team.
|
// This indirectly verifies whether the user has access to the team.
|
||||||
const settings = await getTeamSettings({ userId, teamId });
|
const settings = await getTeamSettings({ userId, teamId });
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
const parentFolder = await prisma.folder.findFirst({
|
||||||
|
where: {
|
||||||
|
id: parentId,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parentFolder) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Parent folder not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentFolder.type !== type) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Parent folder type does not match the folder type',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.folder.create({
|
return await prisma.folder.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||||
import { getTeamById } from '../team/get-team';
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
@@ -20,6 +21,9 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
|||||||
teamId,
|
teamId,
|
||||||
userId,
|
userId,
|
||||||
}),
|
}),
|
||||||
|
visibility: {
|
||||||
|
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
|||||||
|
|
||||||
return await prisma.folder.delete({
|
return await prisma.folder.delete({
|
||||||
where: {
|
where: {
|
||||||
id: folderId,
|
id: folder.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
117
packages/lib/server-only/folder/find-folders-internal.ts
Normal file
117
packages/lib/server-only/folder/find-folders-internal.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||||
|
import type { TFolderType } from '../../types/folder-type';
|
||||||
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
|
export interface FindFoldersInternalOptions {
|
||||||
|
userId: number;
|
||||||
|
teamId: number;
|
||||||
|
parentId?: string | null;
|
||||||
|
type?: TFolderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findFoldersInternal = async ({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
parentId,
|
||||||
|
type,
|
||||||
|
}: FindFoldersInternalOptions) => {
|
||||||
|
const team = await getTeamById({ userId, teamId });
|
||||||
|
|
||||||
|
const visibilityFilters = {
|
||||||
|
visibility: {
|
||||||
|
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const whereClause = {
|
||||||
|
AND: [
|
||||||
|
{ parentId },
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ teamId, ...visibilityFilters },
|
||||||
|
{ userId, teamId },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const folders = await prisma.folder.findMany({
|
||||||
|
where: {
|
||||||
|
...whereClause,
|
||||||
|
...(type ? { type } : {}),
|
||||||
|
},
|
||||||
|
orderBy: [{ pinned: 'desc' }, { createdAt: 'desc' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const foldersWithDetails = await Promise.all(
|
||||||
|
folders.map(async (folder) => {
|
||||||
|
try {
|
||||||
|
const [subfolders, documentCount, templateCount, subfolderCount] = await Promise.all([
|
||||||
|
prisma.folder.findMany({
|
||||||
|
where: {
|
||||||
|
parentId: folder.id,
|
||||||
|
teamId,
|
||||||
|
...visibilityFilters,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.envelope.count({
|
||||||
|
where: {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
folderId: folder.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.envelope.count({
|
||||||
|
where: {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
folderId: folder.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.folder.count({
|
||||||
|
where: {
|
||||||
|
parentId: folder.id,
|
||||||
|
teamId,
|
||||||
|
...visibilityFilters,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const subfoldersWithEmptySubfolders = subfolders.map((subfolder) => ({
|
||||||
|
...subfolder,
|
||||||
|
subfolders: [],
|
||||||
|
_count: {
|
||||||
|
documents: 0,
|
||||||
|
templates: 0,
|
||||||
|
subfolders: 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
subfolders: subfoldersWithEmptySubfolders,
|
||||||
|
_count: {
|
||||||
|
documents: documentCount,
|
||||||
|
templates: templateCount,
|
||||||
|
subfolders: subfolderCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing folder:', folder.id, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return foldersWithDetails;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in findFolders:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { EnvelopeType } from '@prisma/client';
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
import type { TFolderType } from '../../types/folder-type';
|
||||||
|
import type { FindResultResponse } from '../../types/search-params';
|
||||||
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getTeamById } from '../team/get-team';
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
export interface FindFoldersOptions {
|
export interface FindFoldersOptions {
|
||||||
@@ -11,102 +13,48 @@ export interface FindFoldersOptions {
|
|||||||
teamId: number;
|
teamId: number;
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
type?: TFolderType;
|
type?: TFolderType;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => {
|
export const findFolders = async ({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
parentId,
|
||||||
|
type,
|
||||||
|
page = 1,
|
||||||
|
perPage = 10,
|
||||||
|
}: FindFoldersOptions) => {
|
||||||
const team = await getTeamById({ userId, teamId });
|
const team = await getTeamById({ userId, teamId });
|
||||||
|
|
||||||
const visibilityFilters = {
|
const whereClause: Prisma.FolderWhereInput = {
|
||||||
|
parentId,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
type,
|
||||||
visibility: {
|
visibility: {
|
||||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const whereClause = {
|
const [data, count] = await Promise.all([
|
||||||
AND: [
|
|
||||||
{ parentId },
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{ teamId, ...visibilityFilters },
|
|
||||||
{ userId, teamId },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const folders = await prisma.folder.findMany({
|
|
||||||
where: {
|
|
||||||
...whereClause,
|
|
||||||
...(type ? { type } : {}),
|
|
||||||
},
|
|
||||||
orderBy: [{ pinned: 'desc' }, { createdAt: 'desc' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const foldersWithDetails = await Promise.all(
|
|
||||||
folders.map(async (folder) => {
|
|
||||||
try {
|
|
||||||
const [subfolders, documentCount, templateCount, subfolderCount] = await Promise.all([
|
|
||||||
prisma.folder.findMany({
|
prisma.folder.findMany({
|
||||||
where: {
|
where: whereClause,
|
||||||
parentId: folder.id,
|
skip: Math.max(page - 1, 0) * perPage,
|
||||||
teamId,
|
take: perPage,
|
||||||
...visibilityFilters,
|
|
||||||
},
|
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.envelope.count({
|
|
||||||
where: {
|
|
||||||
type: EnvelopeType.DOCUMENT,
|
|
||||||
folderId: folder.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.envelope.count({
|
|
||||||
where: {
|
|
||||||
type: EnvelopeType.TEMPLATE,
|
|
||||||
folderId: folder.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.folder.count({
|
prisma.folder.count({
|
||||||
where: {
|
where: whereClause,
|
||||||
parentId: folder.id,
|
|
||||||
teamId,
|
|
||||||
...visibilityFilters,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const subfoldersWithEmptySubfolders = subfolders.map((subfolder) => ({
|
|
||||||
...subfolder,
|
|
||||||
subfolders: [],
|
|
||||||
_count: {
|
|
||||||
documents: 0,
|
|
||||||
templates: 0,
|
|
||||||
subfolders: 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...folder,
|
data,
|
||||||
subfolders: subfoldersWithEmptySubfolders,
|
count,
|
||||||
_count: {
|
currentPage: Math.max(page, 1),
|
||||||
documents: documentCount,
|
perPage,
|
||||||
templates: templateCount,
|
totalPages: Math.ceil(count / perPage),
|
||||||
subfolders: subfolderCount,
|
} satisfies FindResultResponse<typeof data>;
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing folder:', folder.id, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return foldersWithDetails;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in findFolders:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,51 +1,30 @@
|
|||||||
import { TeamMemberRole } from '@prisma/client';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { DocumentVisibility } from '../../types/document-visibility';
|
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
import type { TFolderType } from '../../types/folder-type';
|
||||||
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getTeamById } from '../team/get-team';
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
export interface GetFolderByIdOptions {
|
export interface GetFolderByIdOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
folderId?: string;
|
folderId: string;
|
||||||
type?: TFolderType;
|
type?: TFolderType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFolderById = async ({ userId, teamId, folderId, type }: GetFolderByIdOptions) => {
|
export const getFolderById = async ({ userId, teamId, folderId, type }: GetFolderByIdOptions) => {
|
||||||
const team = await getTeamById({ userId, teamId });
|
const team = await getTeamById({ userId, teamId });
|
||||||
|
|
||||||
const visibilityFilters = match(team.currentTeamRole)
|
|
||||||
.with(TeamMemberRole.ADMIN, () => ({
|
|
||||||
visibility: {
|
|
||||||
in: [
|
|
||||||
DocumentVisibility.EVERYONE,
|
|
||||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
|
||||||
DocumentVisibility.ADMIN,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.with(TeamMemberRole.MANAGER, () => ({
|
|
||||||
visibility: {
|
|
||||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE }));
|
|
||||||
|
|
||||||
const whereClause = {
|
|
||||||
id: folderId,
|
|
||||||
...(type ? { type } : {}),
|
|
||||||
OR: [
|
|
||||||
{ teamId, ...visibilityFilters },
|
|
||||||
{ userId, teamId },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const folder = await prisma.folder.findFirst({
|
const folder = await prisma.folder.findFirst({
|
||||||
where: whereClause,
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
type,
|
||||||
|
visibility: {
|
||||||
|
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
|
||||||
|
|
||||||
export interface MoveFolderOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
folderId?: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
requestMetadata?: ApiRequestMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const moveFolder = async ({ userId, teamId, folderId, parentId }: MoveFolderOptions) => {
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
|
||||||
const folder = await tx.folder.findFirst({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
team: buildTeamWhereQuery({
|
|
||||||
teamId,
|
|
||||||
userId,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Folder not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentId) {
|
|
||||||
const parentFolder = await tx.folder.findFirst({
|
|
||||||
where: {
|
|
||||||
id: parentId,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
type: folder.type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parentFolder) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Parent folder not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentId === folderId) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Cannot move a folder into itself',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentParentId = parentFolder.parentId;
|
|
||||||
while (currentParentId) {
|
|
||||||
if (currentParentId === folderId) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
||||||
message: 'Cannot move a folder into its descendant',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentParent = await tx.folder.findUnique({
|
|
||||||
where: {
|
|
||||||
id: currentParentId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
parentId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentParent) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentParentId = currentParent.parentId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await tx.folder.update({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
parentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
|
||||||
|
|
||||||
export interface PinFolderOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
folderId: string;
|
|
||||||
type?: TFolderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pinFolder = async ({ userId, teamId, folderId, type }: PinFolderOptions) => {
|
|
||||||
const folder = await prisma.folder.findFirst({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
team: buildTeamWhereQuery({
|
|
||||||
teamId,
|
|
||||||
userId,
|
|
||||||
}),
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Folder not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.folder.update({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
pinned: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
|
||||||
|
|
||||||
export interface UnpinFolderOptions {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
folderId: string;
|
|
||||||
type?: TFolderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const unpinFolder = async ({ userId, teamId, folderId, type }: UnpinFolderOptions) => {
|
|
||||||
const folder = await prisma.folder.findFirst({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
team: buildTeamWhereQuery({
|
|
||||||
teamId,
|
|
||||||
userId,
|
|
||||||
}),
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Folder not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.folder.update({
|
|
||||||
where: {
|
|
||||||
id: folderId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
pinned: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentVisibility } from '@documenso/prisma/generated/types';
|
import type { DocumentVisibility } from '@documenso/prisma/generated/types';
|
||||||
|
|
||||||
import type { TFolderType } from '../../types/folder-type';
|
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||||
import { FolderType } from '../../types/folder-type';
|
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
export interface UpdateFolderOptions {
|
export interface UpdateFolderOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId: number;
|
||||||
folderId: string;
|
folderId: string;
|
||||||
name: string;
|
data: {
|
||||||
visibility: DocumentVisibility;
|
parentId?: string | null;
|
||||||
type?: TFolderType;
|
name?: string;
|
||||||
|
visibility?: DocumentVisibility;
|
||||||
|
pinned?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateFolder = async ({
|
export const updateFolder = async ({ userId, teamId, folderId, data }: UpdateFolderOptions) => {
|
||||||
userId,
|
const { parentId, name, visibility, pinned } = data;
|
||||||
teamId,
|
|
||||||
folderId,
|
const team = await getTeamById({ userId, teamId });
|
||||||
name,
|
|
||||||
visibility,
|
|
||||||
type,
|
|
||||||
}: UpdateFolderOptions) => {
|
|
||||||
const folder = await prisma.folder.findFirst({
|
const folder = await prisma.folder.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: folderId,
|
id: folderId,
|
||||||
@@ -30,7 +30,9 @@ export const updateFolder = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
userId,
|
userId,
|
||||||
}),
|
}),
|
||||||
type,
|
visibility: {
|
||||||
|
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,17 +42,66 @@ export const updateFolder = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTemplateFolder = folder.type === FolderType.TEMPLATE;
|
if (parentId) {
|
||||||
const effectiveVisibility =
|
const parentFolder = await prisma.folder.findFirst({
|
||||||
isTemplateFolder && teamId !== null ? DocumentVisibility.EVERYONE : visibility;
|
where: {
|
||||||
|
id: parentId,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
type: folder.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parentFolder) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Parent folder not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentId === folderId) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Cannot move a folder into itself',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentParentId = parentFolder.parentId;
|
||||||
|
|
||||||
|
while (currentParentId) {
|
||||||
|
if (currentParentId === folderId) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Cannot move a folder into its descendant',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentParent = await prisma.folder.findUnique({
|
||||||
|
where: {
|
||||||
|
id: currentParentId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
parentId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentParent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentParentId = currentParent.parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.folder.update({
|
return await prisma.folder.update({
|
||||||
where: {
|
where: {
|
||||||
id: folderId,
|
id: folderId,
|
||||||
|
team: buildTeamWhereQuery({
|
||||||
|
teamId,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
visibility: effectiveVisibility,
|
visibility,
|
||||||
|
parentId,
|
||||||
|
pinned,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
202
packages/lib/server-only/pdf/auto-place-fields.ts
Normal file
202
packages/lib/server-only/pdf/auto-place-fields.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||||
|
import PDFParser from 'pdf2json';
|
||||||
|
|
||||||
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
|
type TextPosition = {
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CharIndexMapping = {
|
||||||
|
textPosIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaceholderInfo = {
|
||||||
|
placeholder: string;
|
||||||
|
fieldType: string;
|
||||||
|
recipient: string;
|
||||||
|
isRequired: string;
|
||||||
|
page: number;
|
||||||
|
// PDF2JSON coordinates (in page units - these are relative to page dimensions)
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
// Page dimensions from PDF2JSON (in page units)
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Questions for later:
|
||||||
|
- Does it handle multi-page PDFs?
|
||||||
|
- What happens with incorrect placeholders? E.g. those containing non-accepted properties.
|
||||||
|
- The placeholder data is dynamic. How to handle this parsing? Perhaps we need to do it similar to the fieldMeta parsing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new PDFParser(null, true);
|
||||||
|
|
||||||
|
parser.on('pdfParser_dataError', (errData) => {
|
||||||
|
reject(errData);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('pdfParser_dataReady', (pdfData) => {
|
||||||
|
const placeholders: PlaceholderInfo[] = [];
|
||||||
|
|
||||||
|
pdfData.Pages.forEach((page, pageIndex) => {
|
||||||
|
/*
|
||||||
|
pdf2json returns the PDF page content as an array of characters.
|
||||||
|
We need to concatenate the characters to get the full text.
|
||||||
|
We also need to get the position of the text so we can place the placeholders in the correct position.
|
||||||
|
|
||||||
|
Page dimensions from PDF2JSON are in "page units" (relative coordinates)
|
||||||
|
*/
|
||||||
|
const pageWidth = page.Width;
|
||||||
|
const pageHeight = page.Height;
|
||||||
|
|
||||||
|
let pageText = '';
|
||||||
|
const textPositions: TextPosition[] = [];
|
||||||
|
const charIndexToTextPos: CharIndexMapping[] = [];
|
||||||
|
|
||||||
|
page.Texts.forEach((text) => {
|
||||||
|
/*
|
||||||
|
R is an array that contains objects with each character.
|
||||||
|
The decodedText contains only the character, without any other information.
|
||||||
|
|
||||||
|
textPositions stores each character and its position on the page.
|
||||||
|
*/
|
||||||
|
const decodedText = text.R.map((run) => decodeURIComponent(run.T)).join('');
|
||||||
|
|
||||||
|
for (let i = 0; i < decodedText.length; i++) {
|
||||||
|
charIndexToTextPos.push({
|
||||||
|
textPosIndex: textPositions.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pageText += decodedText;
|
||||||
|
|
||||||
|
textPositions.push({
|
||||||
|
text: decodedText,
|
||||||
|
x: text.x,
|
||||||
|
y: text.y,
|
||||||
|
w: text.w || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderMatches = pageText.matchAll(/{{([^}]+)}}/g);
|
||||||
|
|
||||||
|
for (const match of placeholderMatches) {
|
||||||
|
const placeholder = match[0];
|
||||||
|
const placeholderData = match[1].split(',').map((part) => part.trim());
|
||||||
|
|
||||||
|
const [fieldType, recipient, isRequired] = placeholderData;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Find the position of where the placeholder starts in the text
|
||||||
|
|
||||||
|
Then find the position of where the placeholder ends in the text by adding the length of the placeholder to the index of the placeholder.
|
||||||
|
*/
|
||||||
|
const matchIndex = match.index;
|
||||||
|
const placeholderLength = placeholder.length;
|
||||||
|
const placeholderEndIndex = matchIndex + placeholderLength;
|
||||||
|
|
||||||
|
const startCharInfo = charIndexToTextPos[matchIndex];
|
||||||
|
const endCharInfo = charIndexToTextPos[placeholderEndIndex - 1];
|
||||||
|
|
||||||
|
if (!startCharInfo || !endCharInfo) {
|
||||||
|
console.error('Could not find text position for placeholder', placeholder);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTextPos = textPositions[startCharInfo.textPosIndex];
|
||||||
|
const endTextPos = textPositions[endCharInfo.textPosIndex];
|
||||||
|
|
||||||
|
/*
|
||||||
|
PDF2JSON coordinates - these are in "page units" (relative coordinates)
|
||||||
|
Calculate width as the distance from start to end, plus a portion of the last character's width
|
||||||
|
Use 10% of the last character width to avoid extending too far beyond the placeholder
|
||||||
|
*/
|
||||||
|
const x = startTextPos.x;
|
||||||
|
const y = startTextPos.y;
|
||||||
|
const width = endTextPos.x + endTextPos.w * 0.1 - startTextPos.x;
|
||||||
|
|
||||||
|
placeholders.push({
|
||||||
|
placeholder,
|
||||||
|
fieldType,
|
||||||
|
recipient,
|
||||||
|
isRequired,
|
||||||
|
page: pageIndex + 1,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(placeholders);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.parseBuffer(pdf);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise<Buffer> => {
|
||||||
|
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.load(new Uint8Array(pdf));
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
for (const placeholder of placeholders) {
|
||||||
|
const pageIndex = placeholder.page - 1;
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
|
||||||
|
const { width: pdfLibPageWidth, height: pdfLibPageHeight } = getPageSize(page);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert PDF2JSON coordinates to pdf-lib coordinates:
|
||||||
|
|
||||||
|
PDF2JSON uses relative "page units":
|
||||||
|
- x, y, width, height are in page units
|
||||||
|
- Page dimensions (Width, Height) are also in page units
|
||||||
|
|
||||||
|
pdf-lib uses absolute points (1 point = 1/72 inch):
|
||||||
|
- Need to convert from page units to points
|
||||||
|
- Y-axis in pdf-lib is bottom-up (origin at bottom-left)
|
||||||
|
- Y-axis in PDF2JSON is top-down (origin at top-left)
|
||||||
|
|
||||||
|
Conversion formulas:
|
||||||
|
- x_points = (x / pageWidth) * pdfLibPageWidth
|
||||||
|
- y_points = pdfLibPageHeight - ((y / pageHeight) * pdfLibPageHeight)
|
||||||
|
- width_points = (width / pageWidth) * pdfLibPageWidth
|
||||||
|
- height_points = (height / pageHeight) * pdfLibPageHeight
|
||||||
|
*/
|
||||||
|
|
||||||
|
const xPoints = (placeholder.x / placeholder.pageWidth) * pdfLibPageWidth;
|
||||||
|
const yPoints = pdfLibPageHeight - (placeholder.y / placeholder.pageHeight) * pdfLibPageHeight;
|
||||||
|
const widthPoints = (placeholder.width / placeholder.pageWidth) * pdfLibPageWidth;
|
||||||
|
const heightPoints = (placeholder.height / placeholder.pageHeight) * pdfLibPageHeight;
|
||||||
|
|
||||||
|
page.drawRectangle({
|
||||||
|
x: xPoints,
|
||||||
|
y: yPoints - heightPoints, // Adjust for height since y is at baseline
|
||||||
|
width: widthPoints,
|
||||||
|
height: heightPoints,
|
||||||
|
color: rgb(1, 1, 1),
|
||||||
|
borderColor: rgb(1, 1, 1),
|
||||||
|
borderWidth: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedPdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
return Buffer.from(modifiedPdfBytes);
|
||||||
|
};
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||||
|
|
||||||
export const getCompletedDocumentsMonthly = async () => {
|
export const getCompletedDocumentsMonthly = async () => {
|
||||||
const qb = kyselyPrisma.$kysely
|
const qb = kyselyPrisma.$kysely
|
||||||
.selectFrom('Document')
|
.selectFrom('Envelope')
|
||||||
.select(({ fn }) => [
|
.select(({ fn }) => [
|
||||||
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']).as('month'),
|
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']).as('month'),
|
||||||
fn.count('id').as('count'),
|
fn.count('id').as('count'),
|
||||||
fn
|
fn
|
||||||
.sum(fn.count('id'))
|
.sum(fn.count('id'))
|
||||||
// Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner
|
// Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||||
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']) as any))
|
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']) as any))
|
||||||
.as('cume_count'),
|
.as('cume_count'),
|
||||||
])
|
])
|
||||||
.where(() => sql`"Document"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
|
.where(() => sql`"Envelope"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
|
||||||
|
.where(() => sql`"Envelope"."type" = ${EnvelopeType.DOCUMENT}::"EnvelopeType"`)
|
||||||
.groupBy('month')
|
.groupBy('month')
|
||||||
.orderBy('month', 'desc')
|
.orderBy('month', 'desc')
|
||||||
.limit(12);
|
.limit(12);
|
||||||
|
|||||||
@@ -2,27 +2,26 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|||||||
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
|
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
|
||||||
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
|
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
|
||||||
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
|
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
|
||||||
|
import { findFoldersInternal } from '@documenso/lib/server-only/folder/find-folders-internal';
|
||||||
import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-folder-breadcrumbs';
|
import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-folder-breadcrumbs';
|
||||||
import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id';
|
import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id';
|
||||||
import { moveFolder } from '@documenso/lib/server-only/folder/move-folder';
|
|
||||||
import { pinFolder } from '@documenso/lib/server-only/folder/pin-folder';
|
|
||||||
import { unpinFolder } from '@documenso/lib/server-only/folder/unpin-folder';
|
|
||||||
import { updateFolder } from '@documenso/lib/server-only/folder/update-folder';
|
import { updateFolder } from '@documenso/lib/server-only/folder/update-folder';
|
||||||
|
|
||||||
import { authenticatedProcedure, router } from '../trpc';
|
import { authenticatedProcedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
ZCreateFolderSchema,
|
ZCreateFolderRequestSchema,
|
||||||
ZDeleteFolderSchema,
|
ZCreateFolderResponseSchema,
|
||||||
|
ZDeleteFolderRequestSchema,
|
||||||
|
ZFindFoldersInternalRequestSchema,
|
||||||
|
ZFindFoldersInternalResponseSchema,
|
||||||
ZFindFoldersRequestSchema,
|
ZFindFoldersRequestSchema,
|
||||||
ZFindFoldersResponseSchema,
|
ZFindFoldersResponseSchema,
|
||||||
ZGenericSuccessResponse,
|
ZGenericSuccessResponse,
|
||||||
ZGetFoldersResponseSchema,
|
ZGetFoldersResponseSchema,
|
||||||
ZGetFoldersSchema,
|
ZGetFoldersSchema,
|
||||||
ZMoveFolderSchema,
|
|
||||||
ZPinFolderSchema,
|
|
||||||
ZSuccessResponseSchema,
|
ZSuccessResponseSchema,
|
||||||
ZUnpinFolderSchema,
|
ZUpdateFolderRequestSchema,
|
||||||
ZUpdateFolderSchema,
|
ZUpdateFolderResponseSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
export const folderRouter = router({
|
export const folderRouter = router({
|
||||||
@@ -43,7 +42,7 @@ export const folderRouter = router({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const folders = await findFolders({
|
const folders = await findFoldersInternal({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId,
|
teamId,
|
||||||
parentId,
|
parentId,
|
||||||
@@ -67,11 +66,47 @@ export const folderRouter = router({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @public
|
||||||
*/
|
*/
|
||||||
findFolders: authenticatedProcedure
|
findFolders: authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/folder',
|
||||||
|
summary: 'Find folders',
|
||||||
|
description: 'Find folders based on a search criteria',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(ZFindFoldersRequestSchema)
|
.input(ZFindFoldersRequestSchema)
|
||||||
.output(ZFindFoldersResponseSchema)
|
.output(ZFindFoldersResponseSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { teamId, user } = ctx;
|
||||||
|
const { parentId, type, page, perPage } = input;
|
||||||
|
|
||||||
|
ctx.logger.info({
|
||||||
|
input: {
|
||||||
|
parentId,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await findFolders({
|
||||||
|
userId: user.id,
|
||||||
|
teamId,
|
||||||
|
parentId,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
findFoldersInternal: authenticatedProcedure
|
||||||
|
.input(ZFindFoldersInternalRequestSchema)
|
||||||
|
.output(ZFindFoldersInternalResponseSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { teamId, user } = ctx;
|
const { teamId, user } = ctx;
|
||||||
const { parentId, type } = input;
|
const { parentId, type } = input;
|
||||||
@@ -83,7 +118,7 @@ export const folderRouter = router({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const folders = await findFolders({
|
const folders = await findFoldersInternal({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId,
|
teamId,
|
||||||
parentId,
|
parentId,
|
||||||
@@ -107,10 +142,20 @@ export const folderRouter = router({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @public
|
||||||
*/
|
*/
|
||||||
createFolder: authenticatedProcedure
|
createFolder: authenticatedProcedure
|
||||||
.input(ZCreateFolderSchema)
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/folder/create',
|
||||||
|
summary: 'Create new folder',
|
||||||
|
description: 'Creates a new folder in your team',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(ZCreateFolderRequestSchema)
|
||||||
|
.output(ZCreateFolderResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { teamId, user } = ctx;
|
const { teamId, user } = ctx;
|
||||||
const { name, parentId, type } = input;
|
const { name, parentId, type } = input;
|
||||||
@@ -145,181 +190,77 @@ export const folderRouter = router({
|
|||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
...result,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @public
|
||||||
*/
|
*/
|
||||||
updateFolder: authenticatedProcedure
|
updateFolder: authenticatedProcedure
|
||||||
.input(ZUpdateFolderSchema)
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/folder/update',
|
||||||
|
summary: 'Update folder',
|
||||||
|
description: 'Updates an existing folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(ZUpdateFolderRequestSchema)
|
||||||
|
.output(ZUpdateFolderResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { teamId, user } = ctx;
|
const { teamId, user } = ctx;
|
||||||
const { id, name, visibility } = input;
|
const { folderId, data } = input;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
id,
|
folderId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentFolder = await getFolderById({
|
|
||||||
userId: user.id,
|
|
||||||
teamId,
|
|
||||||
folderId: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await updateFolder({
|
const result = await updateFolder({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId,
|
teamId,
|
||||||
folderId: id,
|
folderId,
|
||||||
name,
|
data,
|
||||||
visibility,
|
|
||||||
type: currentFolder.type,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
type: currentFolder.type,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @public
|
||||||
*/
|
*/
|
||||||
deleteFolder: authenticatedProcedure
|
deleteFolder: authenticatedProcedure
|
||||||
.input(ZDeleteFolderSchema)
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/folder/delete',
|
||||||
|
summary: 'Delete folder',
|
||||||
|
description: 'Deletes an existing folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(ZDeleteFolderRequestSchema)
|
||||||
.output(ZSuccessResponseSchema)
|
.output(ZSuccessResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { teamId, user } = ctx;
|
const { teamId, user } = ctx;
|
||||||
const { id } = input;
|
const { folderId } = input;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
id,
|
folderId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await deleteFolder({
|
await deleteFolder({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId,
|
teamId,
|
||||||
folderId: id,
|
folderId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ZGenericSuccessResponse;
|
return ZGenericSuccessResponse;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
moveFolder: authenticatedProcedure.input(ZMoveFolderSchema).mutation(async ({ input, ctx }) => {
|
|
||||||
const { teamId, user } = ctx;
|
|
||||||
const { id, parentId } = input;
|
|
||||||
|
|
||||||
ctx.logger.info({
|
|
||||||
input: {
|
|
||||||
id,
|
|
||||||
parentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentFolder = await getFolderById({
|
|
||||||
userId: user.id,
|
|
||||||
teamId,
|
|
||||||
folderId: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parentId !== null) {
|
|
||||||
try {
|
|
||||||
await getFolderById({
|
|
||||||
userId: user.id,
|
|
||||||
teamId,
|
|
||||||
folderId: parentId,
|
|
||||||
type: currentFolder.type,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Parent folder not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await moveFolder({
|
|
||||||
userId: user.id,
|
|
||||||
teamId,
|
|
||||||
folderId: id,
|
|
||||||
parentId,
|
|
||||||
requestMetadata: ctx.metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
type: currentFolder.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
pinFolder: authenticatedProcedure.input(ZPinFolderSchema).mutation(async ({ ctx, input }) => {
|
|
||||||
const { folderId } = input;
|
|
||||||
|
|
||||||
ctx.logger.info({
|
|
||||||
input: {
|
|
||||||
folderId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentFolder = await getFolderById({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
teamId: ctx.teamId,
|
|
||||||
folderId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await pinFolder({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
teamId: ctx.teamId,
|
|
||||||
folderId,
|
|
||||||
type: currentFolder.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
type: currentFolder.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
unpinFolder: authenticatedProcedure.input(ZUnpinFolderSchema).mutation(async ({ ctx, input }) => {
|
|
||||||
const { folderId } = input;
|
|
||||||
|
|
||||||
ctx.logger.info({
|
|
||||||
input: {
|
|
||||||
folderId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentFolder = await getFolderById({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
teamId: ctx.teamId,
|
|
||||||
folderId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await unpinFolder({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
teamId: ctx.teamId,
|
|
||||||
folderId,
|
|
||||||
type: currentFolder.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
type: currentFolder.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type';
|
import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type';
|
||||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import { DocumentVisibility } from '@documenso/prisma/generated/types';
|
import { DocumentVisibility } from '@documenso/prisma/generated/types';
|
||||||
|
import FolderSchema from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required for empty responses since we currently can't 201 requests for our openapi setup.
|
* Required for empty responses since we currently can't 201 requests for our openapi setup.
|
||||||
@@ -11,24 +12,23 @@ import { DocumentVisibility } from '@documenso/prisma/generated/types';
|
|||||||
*/
|
*/
|
||||||
export const ZSuccessResponseSchema = z.object({
|
export const ZSuccessResponseSchema = z.object({
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
type: ZFolderTypeSchema.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZGenericSuccessResponse = {
|
export const ZGenericSuccessResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
} satisfies z.infer<typeof ZSuccessResponseSchema>;
|
} satisfies z.infer<typeof ZSuccessResponseSchema>;
|
||||||
|
|
||||||
export const ZFolderSchema = z.object({
|
export const ZFolderSchema = FolderSchema.pick({
|
||||||
id: z.string(),
|
id: true,
|
||||||
name: z.string(),
|
name: true,
|
||||||
userId: z.number(),
|
userId: true,
|
||||||
teamId: z.number().nullable(),
|
teamId: true,
|
||||||
parentId: z.string().nullable(),
|
parentId: true,
|
||||||
pinned: z.boolean(),
|
pinned: true,
|
||||||
createdAt: z.date(),
|
createdAt: true,
|
||||||
updatedAt: z.date(),
|
updatedAt: true,
|
||||||
visibility: z.nativeEnum(DocumentVisibility),
|
visibility: true,
|
||||||
type: ZFolderTypeSchema,
|
type: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFolder = z.infer<typeof ZFolderSchema>;
|
export type TFolder = z.infer<typeof ZFolderSchema>;
|
||||||
@@ -51,40 +51,39 @@ export const ZFolderWithSubfoldersSchema = ZFolderSchema.extend({
|
|||||||
|
|
||||||
export type TFolderWithSubfolders = z.infer<typeof ZFolderWithSubfoldersSchema>;
|
export type TFolderWithSubfolders = z.infer<typeof ZFolderWithSubfoldersSchema>;
|
||||||
|
|
||||||
export const ZCreateFolderSchema = z.object({
|
const ZFolderParentIdSchema = z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'The folder ID to place this folder within. Leave empty to place folder at the root level.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ZCreateFolderRequestSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
parentId: z.string().optional(),
|
parentId: ZFolderParentIdSchema.optional(),
|
||||||
type: ZFolderTypeSchema.optional(),
|
type: ZFolderTypeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateFolderSchema = z.object({
|
export const ZCreateFolderResponseSchema = ZFolderSchema;
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
export const ZUpdateFolderRequestSchema = z.object({
|
||||||
visibility: z.nativeEnum(DocumentVisibility),
|
folderId: z.string().describe('The ID of the folder to update'),
|
||||||
type: ZFolderTypeSchema.optional(),
|
data: z.object({
|
||||||
|
name: z.string().optional().describe('The name of the folder'),
|
||||||
|
parentId: ZFolderParentIdSchema.optional().nullable(),
|
||||||
|
visibility: z
|
||||||
|
.nativeEnum(DocumentVisibility)
|
||||||
|
.optional()
|
||||||
|
.describe('The visibility of the folder'),
|
||||||
|
pinned: z.boolean().optional().describe('Whether the folder should be pinned'),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUpdateFolderSchema = z.infer<typeof ZUpdateFolderSchema>;
|
export type TUpdateFolderRequestSchema = z.infer<typeof ZUpdateFolderRequestSchema>;
|
||||||
|
|
||||||
export const ZDeleteFolderSchema = z.object({
|
export const ZUpdateFolderResponseSchema = ZFolderSchema;
|
||||||
id: z.string(),
|
|
||||||
type: ZFolderTypeSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZMoveFolderSchema = z.object({
|
export const ZDeleteFolderRequestSchema = z.object({
|
||||||
id: z.string(),
|
|
||||||
parentId: z.string().nullable(),
|
|
||||||
type: ZFolderTypeSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZPinFolderSchema = z.object({
|
|
||||||
folderId: z.string(),
|
folderId: z.string(),
|
||||||
type: ZFolderTypeSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZUnpinFolderSchema = z.object({
|
|
||||||
folderId: z.string(),
|
|
||||||
type: ZFolderTypeSchema.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZGetFoldersSchema = z.object({
|
export const ZGetFoldersSchema = z.object({
|
||||||
@@ -101,11 +100,20 @@ export const ZGetFoldersResponseSchema = z.object({
|
|||||||
export type TGetFoldersResponse = z.infer<typeof ZGetFoldersResponseSchema>;
|
export type TGetFoldersResponse = z.infer<typeof ZGetFoldersResponseSchema>;
|
||||||
|
|
||||||
export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({
|
export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||||
|
parentId: z.string().optional().describe('Filter folders by the parent folder ID'),
|
||||||
|
type: ZFolderTypeSchema.optional().describe('Filter folders by the folder type'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZFindFoldersResponseSchema = ZFindResultResponse.extend({
|
||||||
|
data: z.array(ZFolderSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZFindFoldersInternalRequestSchema = ZFindSearchParamsSchema.extend({
|
||||||
parentId: z.string().nullable().optional(),
|
parentId: z.string().nullable().optional(),
|
||||||
type: ZFolderTypeSchema.optional(),
|
type: ZFolderTypeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZFindFoldersResponseSchema = z.object({
|
export const ZFindFoldersInternalResponseSchema = z.object({
|
||||||
data: z.array(ZFolderWithSubfoldersSchema),
|
data: z.array(ZFolderWithSubfoldersSchema),
|
||||||
breadcrumbs: z.array(ZFolderSchema),
|
breadcrumbs: z.array(ZFolderSchema),
|
||||||
type: ZFolderTypeSchema.optional(),
|
type: ZFolderTypeSchema.optional(),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
|
|
||||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||||
|
|||||||
Reference in New Issue
Block a user