mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 19:21:39 +10:00
Merge branch 'main' into feat/delete-archive
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -81,4 +81,4 @@
|
||||
"next": "$next"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
apps/web/process-env.d.ts
vendored
2
apps/web/process-env.d.ts
vendored
@ -16,5 +16,7 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID: string;
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET: string;
|
||||
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
documentStatus={document.status}
|
||||
/>
|
||||
|
||||
<DownloadAuditLogButton documentId={document.id} />
|
||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,10 +9,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DownloadAuditLogButtonProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
||||
export const DownloadAuditLogButton = ({
|
||||
className,
|
||||
teamId,
|
||||
documentId,
|
||||
}: DownloadAuditLogButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isLoading } =
|
||||
@ -20,7 +25,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadAuditLogs({ documentId });
|
||||
const { url } = await downloadAuditLogs({ teamId, documentId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
@ -41,6 +42,7 @@ export const DeleteDocumentDialog = ({
|
||||
const router = useRouter();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { refreshLimits } = useLimits();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
@ -48,6 +50,7 @@ export const DeleteDocumentDialog = ({
|
||||
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document deleted',
|
||||
|
||||
@ -36,7 +36,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { quota, remaining } = useLimits();
|
||||
const { quota, remaining, refreshLimits } = useLimits();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@ -71,6 +71,8 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: 'Document uploaded',
|
||||
description: 'Your document has been uploaded successfully.',
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
@ -39,6 +39,7 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateToken,
|
||||
}: TemplatesDirectPageViewProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -82,8 +83,15 @@ export const DirectTemplatePageView = ({
|
||||
|
||||
const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
|
||||
try {
|
||||
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
|
||||
|
||||
if (directTemplateExternalId) {
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
const token = await createDocumentFromDirectTemplate({
|
||||
directTemplateToken,
|
||||
directTemplateExternalId,
|
||||
directRecipientName: fullName,
|
||||
directRecipientEmail: recipient.email,
|
||||
templateUpdatedAt: template.updatedAt,
|
||||
|
||||
@ -41,6 +41,7 @@ import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
@ -182,6 +183,15 @@ export const SignDirectTemplateForm = ({
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
key={field.id}
|
||||
|
||||
@ -13,6 +13,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@ -25,6 +26,7 @@ import { truncateTitle } from '~/helpers/truncate-title';
|
||||
import { SigningAuthPageView } from '../signing-auth-page';
|
||||
import { ClaimAccount } from './claim-account';
|
||||
import { DocumentPreviewButton } from './document-preview-button';
|
||||
import { PollUntilDocumentCompleted } from './poll-until-document-completed';
|
||||
|
||||
export type CompletedSigningPageProps = {
|
||||
params: {
|
||||
@ -77,6 +79,9 @@ export default async function CompletedSigningPage({
|
||||
}
|
||||
|
||||
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
|
||||
const isExistingUser = await getUserByEmail({ email: recipient.email })
|
||||
.then((u) => !!u)
|
||||
.catch(() => false);
|
||||
|
||||
const recipientName =
|
||||
recipient.name ||
|
||||
@ -85,7 +90,7 @@ export default async function CompletedSigningPage({
|
||||
|
||||
const sessionData = await getServerSession();
|
||||
const isLoggedIn = !!sessionData?.user;
|
||||
const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -201,6 +206,8 @@ export default async function CompletedSigningPage({
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PollUntilDocumentCompleted document={document} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { Document } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type PollUntilDocumentCompletedProps = {
|
||||
document: Pick<Document, 'id' | 'status' | 'deletedAt'>;
|
||||
};
|
||||
|
||||
export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentCompletedProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (window.document.hasFocus()) {
|
||||
router.refresh();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [router, document.status]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
140
apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type InitialsFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const InitialsField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: InitialsFieldProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { fullName } = useRequiredSigningContext();
|
||||
const initials = extractInitials(fullName);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const {
|
||||
mutateAsync: removeSignedFieldWithToken,
|
||||
isLoading: isRemoveSignedFieldWithTokenLoading,
|
||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||
try {
|
||||
const value = initials ?? '';
|
||||
|
||||
const payload: TSignFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value,
|
||||
isBase64: false,
|
||||
authOptions,
|
||||
};
|
||||
|
||||
if (onSignField) {
|
||||
await onSignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await signFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while signing the document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = async () => {
|
||||
try {
|
||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
};
|
||||
|
||||
if (onUnsignField) {
|
||||
await onUnsignField(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeSignedFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while removing the signature.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials">
|
||||
{isLoading && (
|
||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!field.inserted && (
|
||||
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
|
||||
Initials
|
||||
</p>
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
||||
{field.customText}
|
||||
</p>
|
||||
)}
|
||||
</SigningFieldContainer>
|
||||
);
|
||||
};
|
||||
@ -216,7 +216,10 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
let fieldDisplayName = 'Number';
|
||||
|
||||
if (parsedFieldMeta?.label) {
|
||||
fieldDisplayName = parsedFieldMeta.label.length > 10 ? parsedFieldMeta.label.substring(0, 10) + '...' : parsedFieldMeta.label;
|
||||
fieldDisplayName =
|
||||
parsedFieldMeta.label.length > 10
|
||||
? parsedFieldMeta.label.substring(0, 10) + '...'
|
||||
: parsedFieldMeta.label;
|
||||
}
|
||||
|
||||
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
||||
@ -246,7 +249,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center justify-center gap-x-1 text-sm">
|
||||
<Hash className='h-4 w-4' /> {fieldDisplayName}
|
||||
<Hash className="h-4 w-4" /> {fieldDisplayName}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -172,15 +172,15 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
||||
<RadioGroup>
|
||||
{values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
className=""
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`}>
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
<RadioGroupItem
|
||||
className=""
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`}>
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
@ -190,7 +190,7 @@ export const SignatureField = ({
|
||||
)}
|
||||
|
||||
{state === 'empty' && (
|
||||
<p className="group-hover:text-primary font-signature text-muted-foreground duration-200 group-hover:text-yellow-300 text-xl">
|
||||
<p className="group-hover:text-primary font-signature text-muted-foreground text-xl duration-200 group-hover:text-yellow-300">
|
||||
Signature
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -39,7 +39,16 @@ export type SignatureFieldProps = {
|
||||
*/
|
||||
onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise<void> | void;
|
||||
onRemove?: (fieldType?: string) => Promise<void> | void;
|
||||
type?: 'Date' | 'Email' | 'Name' | 'Signature' | 'Radio' | 'Dropdown' | 'Number' | 'Checkbox';
|
||||
type?:
|
||||
| 'Date'
|
||||
| 'Initials'
|
||||
| 'Email'
|
||||
| 'Name'
|
||||
| 'Signature'
|
||||
| 'Radio'
|
||||
| 'Dropdown'
|
||||
| 'Number'
|
||||
| 'Checkbox';
|
||||
tooltipText?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import { DateField } from './date-field';
|
||||
import { DropdownField } from './dropdown-field';
|
||||
import { EmailField } from './email-field';
|
||||
import { SigningForm } from './form';
|
||||
import { InitialsField } from './initials-field';
|
||||
import { NameField } from './name-field';
|
||||
import { NumberField } from './number-field';
|
||||
import { RadioField } from './radio-field';
|
||||
@ -101,6 +102,9 @@ export const SigningPageView = ({
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
|
||||
@ -279,11 +279,13 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
/>
|
||||
</div>
|
||||
|
||||
{parsedFieldMeta?.characterLimit !== undefined && parsedFieldMeta?.characterLimit > 0 && !userInputHasErrors && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{charactersRemaining} characters remaining
|
||||
</div>
|
||||
)}
|
||||
{parsedFieldMeta?.characterLimit !== undefined &&
|
||||
parsedFieldMeta?.characterLimit > 0 &&
|
||||
!userInputHasErrors && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{charactersRemaining} characters remaining
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userInputHasErrors && (
|
||||
<div className="text-sm">
|
||||
|
||||
@ -17,6 +17,10 @@ export default function UnauthenticatedLayout({ children }: UnauthenticatedLayou
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
style={{
|
||||
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
|
||||
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -4,7 +4,11 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import {
|
||||
IS_GOOGLE_SSO_ENABLED,
|
||||
IS_OIDC_SSO_ENABLED,
|
||||
OIDC_PROVIDER_LABEL,
|
||||
} from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
@ -43,6 +47,7 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||
oidcProviderLabel={OIDC_PROVIDER_LABEL}
|
||||
/>
|
||||
|
||||
{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
|
||||
@ -196,6 +196,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
}
|
||||
avatarFallback={formatAvatarFallback(team.name)}
|
||||
primaryText={team.name}
|
||||
textSectionClassName="w-[200px]"
|
||||
secondaryText={
|
||||
<div className="relative w-full">
|
||||
<motion.span
|
||||
|
||||
@ -98,6 +98,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
||||
{
|
||||
type: P.union(
|
||||
FieldType.NAME,
|
||||
FieldType.INITIALS,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NUMBER,
|
||||
FieldType.RADIO,
|
||||
|
||||
@ -71,6 +71,7 @@ export type SignInFormProps = {
|
||||
initialEmail?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
oidcProviderLabel?: string;
|
||||
};
|
||||
|
||||
export const SignInForm = ({
|
||||
@ -78,6 +79,7 @@ export const SignInForm = ({
|
||||
initialEmail,
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
oidcProviderLabel,
|
||||
}: SignInFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const { getFlag } = useFeatureFlags();
|
||||
@ -369,7 +371,7 @@ export const SignInForm = ({
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||
OIDC
|
||||
{oidcProviderLabel || 'OIDC'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@ -29,6 +29,10 @@ export default function NotFoundPartial({ children }: NotFoundPartialProps) {
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="-mr-[50vw] -mt-[15vh] h-full scale-100 object-cover dark:contrast-[70%] dark:invert dark:sepia md:scale-100 lg:scale-[100%]"
|
||||
style={{
|
||||
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
|
||||
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 80%)',
|
||||
}}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@ -6,6 +6,7 @@ import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-cu
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
|
||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { slugify } from '@documenso/lib/utils/slugify';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
@ -60,13 +61,41 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
},
|
||||
linkAccount: async ({ user }) => {
|
||||
linkAccount: async ({ user, account, profile }) => {
|
||||
const userId = typeof user.id === 'string' ? parseInt(user.id) : user.id;
|
||||
|
||||
if (isNaN(userId)) {
|
||||
if (Number.isNaN(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is linking an OIDC account and the email verified date is set then update it in the db.
|
||||
if (account.provider === 'oidc' && profile.emailVerified !== null) {
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
emailVerified: profile.emailVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// auto set public profile name
|
||||
if (account.provider === 'oidc' && user.name && 'url' in user && !user.url) {
|
||||
let counter = 1;
|
||||
let url = slugify(user.name);
|
||||
|
||||
while (await prisma.user.findFirst({ where: { url } })) {
|
||||
url = `${slugify(user.name)}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
|
||||
Reference in New Issue
Block a user