chore: merged main

This commit is contained in:
Catalin Pit
2024-01-25 10:53:05 +02:00
21 changed files with 265 additions and 30 deletions

View File

@ -4,8 +4,10 @@ NEXTAUTH_SECRET="secret"
# [[CRYPTO]] # [[CRYPTO]]
# Application Key for symmetric encryption and decryption # Application Key for symmetric encryption and decryption
# This should be a random string of at least 32 characters # REQUIRED: This should be a random string of at least 32 characters
NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE" NEXT_PRIVATE_ENCRYPTION_KEY=""
# REQUIRED: This should be a random string of at least 32 characters
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=""
# [[AUTH OPTIONAL]] # [[AUTH OPTIONAL]]
NEXT_PRIVATE_GOOGLE_CLIENT_ID="" NEXT_PRIVATE_GOOGLE_CLIENT_ID=""

View File

@ -47,6 +47,14 @@ export const TEAM_MEMBERS = [
engagement: 'Full-Time', engagement: 'Full-Time',
joinDate: 'October 9th, 2023', joinDate: 'October 9th, 2023',
}, },
{
name: 'Adithya Krishna',
role: 'Software Engineer - II',
salary: '-',
location: 'India',
engagement: 'Full-Time',
joinDate: 'December 1st, 2023',
},
]; ];
export const FUNDING_RAISED = [ export const FUNDING_RAISED = [

View File

@ -1,3 +1,4 @@
import { IDENTITY_PROVIDER_NAME } from '@documenso/lib/constants/auth';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app'; import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
@ -17,28 +18,43 @@ export default async function SecuritySettingsPage() {
<hr className="my-4" /> <hr className="my-4" />
<PasswordForm user={user} className="max-w-xl" /> {user.identityProvider === 'DOCUMENSO' ? (
<div>
<PasswordForm user={user} className="max-w-xl" />
<hr className="mb-4 mt-8" /> <hr className="mb-4 mt-8" />
<h4 className="text-lg font-medium">Two Factor Authentication</h4> <h4 className="text-lg font-medium">Two Factor Authentication</h4>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
Add and manage your two factor security settings to add an extra layer of security to your Add and manage your two factor security settings to add an extra layer of security to
account! your account!
</p> </p>
<div className="mt-4 max-w-xl"> <div className="mt-4 max-w-xl">
<h5 className="font-medium">Two-factor methods</h5> <h5 className="font-medium">Two-factor methods</h5>
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} /> <AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
</div> </div>
{user.twoFactorEnabled && ( {user.twoFactorEnabled && (
<div className="mt-4 max-w-xl"> <div className="mt-4 max-w-xl">
<h5 className="font-medium">Recovery methods</h5> <h5 className="font-medium">Recovery methods</h5>
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} /> <RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
</div>
)}
</div>
) : (
<div>
<h4 className="text-lg font-medium">
Your account is managed by {IDENTITY_PROVIDER_NAME[user.identityProvider]}
</h4>
<p className="text-muted-foreground mt-2 text-sm">
To update your password, enable two-factor authentication, and manage other security
settings, please go to your {IDENTITY_PROVIDER_NAME[user.identityProvider]} account
settings.
</p>
</div> </div>
)} )}
</div> </div>

View File

@ -3,6 +3,8 @@ import { redirect } from 'next/navigation';
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
export default function SignUpPage() { export default function SignUpPage() {
@ -21,7 +23,7 @@ export default function SignUpPage() {
signing is within your grasp. signing is within your grasp.
</p> </p>
<SignUpForm className="mt-4" /> <SignUpForm className="mt-4" isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="text-muted-foreground mt-6 text-center text-sm">
Already have an account?{' '} Already have an account?{' '}

View File

@ -252,7 +252,11 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
); );
return THEMES.map((theme) => ( return THEMES.map((theme) => (
<CommandItem key={theme.theme} onSelect={() => setTheme(theme.theme)}> <CommandItem
key={theme.theme}
onSelect={() => setTheme(theme.theme)}
className="mx-2 first:mt-2 last:mb-2"
>
<theme.icon className="mr-2" /> <theme.icon className="mr-2" />
{theme.label} {theme.label}
</CommandItem> </CommandItem>

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { ONE_SECOND } from '@documenso/lib/constants/time'; import { ONE_DAY, ONE_SECOND } from '@documenso/lib/constants/time';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -65,7 +65,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
if (emailVerificationDialogLastShown) { if (emailVerificationDialogLastShown) {
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown); const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
if (Date.now() - lastShownTimestamp < 24 * 60 * 60 * 1000) { if (Date.now() - lastShownTimestamp < ONE_DAY) {
return; return;
} }
} }

View File

@ -112,7 +112,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</Label> </Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled /> <Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
</div> </div>
<FormField <FormField
control={form.control} control={form.control}
name="signature" name="signature"
@ -122,7 +121,10 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
<FormControl> <FormControl>
<SignaturePad <SignaturePad
className="h-44 w-full" className="h-44 w-full"
containerClassName="rounded-lg border bg-background" containerClassName={cn(
'rounded-lg border bg-background',
isSubmitting ? 'pointer-events-none opacity-50' : null,
)}
defaultValue={user.signature ?? undefined} defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')} onChange={(v) => onChange(v ?? '')}
/> />

View File

@ -3,6 +3,7 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod'; import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
@ -23,6 +24,8 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
const SIGN_UP_REDIRECT_PATH = '/documents';
export const ZSignUpFormSchema = z.object({ export const ZSignUpFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().email().min(1), email: z.string().email().min(1),
@ -37,9 +40,10 @@ export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
export type SignUpFormProps = { export type SignUpFormProps = {
className?: string; className?: string;
isGoogleSSOEnabled?: boolean;
}; };
export const SignUpForm = ({ className }: SignUpFormProps) => { export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const analytics = useAnalytics(); const analytics = useAnalytics();
@ -64,7 +68,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
await signIn('credentials', { await signIn('credentials', {
email, email,
password, password,
callbackUrl: '/', callbackUrl: SIGN_UP_REDIRECT_PATH,
}); });
analytics.capture('App: User Sign Up', { analytics.capture('App: User Sign Up', {
@ -89,6 +93,19 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
} }
}; };
const onSignUpWithGoogleClick = async () => {
try {
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
variant: 'destructive',
});
}
};
return ( return (
<Form {...form}> <Form {...form}>
<form <form
@ -166,6 +183,28 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
> >
{isSubmitting ? 'Signing up...' : 'Sign Up'} {isSubmitting ? 'Signing up...' : 'Sign Up'}
</Button> </Button>
{isGoogleSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Sign Up with Google
</Button>
</>
)}
</form> </form>
</Form> </Form>
); );

View File

@ -10,7 +10,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
{isDocument && ( {isDocument && (
<Text className="my-4 text-base text-slate-400"> <Text className="my-4 text-base text-slate-400">
This document was sent using{' '} This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documenso.com"> <Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
Documenso. Documenso.
</Link> </Link>
</Text> </Text>

View File

@ -1,5 +1,12 @@
import { IdentityProvider } from '@documenso/prisma/client';
export const SALT_ROUNDS = 12; export const SALT_ROUNDS = 12;
export const IDENTITY_PROVIDER_NAME: { [key in IdentityProvider]: string } = {
[IdentityProvider.DOCUMENSO]: 'Documenso',
[IdentityProvider.GOOGLE]: 'Google',
};
export const IS_GOOGLE_SSO_ENABLED = Boolean( export const IS_GOOGLE_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET, process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
); );

View File

@ -1 +1,23 @@
export const DOCUMENSO_ENCRYPTION_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_KEY; export const DOCUMENSO_ENCRYPTION_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_KEY;
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY;
if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY or DOCUMENSO_ENCRYPTION_SECONDARY_KEY keys');
}
if (DOCUMENSO_ENCRYPTION_KEY === DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error(
'DOCUMENSO_ENCRYPTION_KEY and DOCUMENSO_ENCRYPTION_SECONDARY_KEY cannot be equal',
);
}
if (DOCUMENSO_ENCRYPTION_KEY === 'CAFEBABE') {
console.warn('*********************************************************************');
console.warn('*');
console.warn('*');
console.warn('Please change the encryption key from the default value of "CAFEBABE"');
console.warn('*');
console.warn('*');
console.warn('*********************************************************************');
}

View File

@ -10,6 +10,7 @@ import GoogleProvider from 'next-auth/providers/google';
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { IdentityProvider } from '@documenso/prisma/client';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble'; import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa'; import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
@ -96,7 +97,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}), }),
], ],
callbacks: { callbacks: {
async jwt({ token, user }) { async jwt({ token, user, trigger, account }) {
const merged = { const merged = {
...token, ...token,
...user, ...user,
@ -141,6 +142,22 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
merged.emailVerified = user.emailVerified?.toISOString() ?? null; merged.emailVerified = user.emailVerified?.toISOString() ?? null;
} }
if ((trigger === 'signIn' || trigger === 'signUp') && account?.provider === 'google') {
merged.emailVerified = user?.emailVerified
? new Date(user.emailVerified).toISOString()
: new Date().toISOString();
await prisma.user.update({
where: {
id: Number(merged.id),
},
data: {
emailVerified: merged.emailVerified,
identityProvider: IdentityProvider.GOOGLE,
},
});
}
return { return {
id: merged.id, id: merged.id,
name: merged.name, name: merged.name,

View File

@ -0,0 +1,33 @@
import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto';
import { ZEncryptedDataSchema } from '@documenso/lib/server-only/crypto/encrypt';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
/**
* Decrypt the passed in data. This uses the secondary encrypt key for miscellaneous data.
*
* @param encryptedData The data encrypted with the `encryptSecondaryData` function.
* @returns The decrypted value, or `null` if the data is invalid or expired.
*/
export const decryptSecondaryData = (encryptedData: string): string | null => {
if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing encryption key');
}
const decryptedBufferValue = symmetricDecrypt({
key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY,
data: encryptedData,
});
const decryptedValue = Buffer.from(decryptedBufferValue).toString('utf-8');
const result = ZEncryptedDataSchema.safeParse(JSON.parse(decryptedValue));
if (!result.success) {
return null;
}
if (result.data.expiresAt !== undefined && result.data.expiresAt < Date.now()) {
return null;
}
return result.data.data;
};

View File

@ -0,0 +1,42 @@
import { z } from 'zod';
import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import type { TEncryptSecondaryDataMutationSchema } from '@documenso/trpc/server/crypto/schema';
export const ZEncryptedDataSchema = z.object({
data: z.string(),
expiresAt: z.number().optional(),
});
export type EncryptDataOptions = {
data: string;
/**
* When the data should no longer be allowed to be decrypted.
*
* Leave this empty to never expire the data.
*/
expiresAt?: number;
};
/**
* Encrypt the passed in data. This uses the secondary encrypt key for miscellaneous data.
*
* @returns The encrypted data.
*/
export const encryptSecondaryData = ({ data, expiresAt }: TEncryptSecondaryDataMutationSchema) => {
if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing encryption key');
}
const dataToEncrypt: z.infer<typeof ZEncryptedDataSchema> = {
data,
expiresAt,
};
return symmetricEncrypt({
key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY,
data: JSON.stringify(dataToEncrypt),
});
};

View File

@ -42,6 +42,11 @@ export const getStats = async ({ user }: GetStatsInput) => {
_all: true, _all: true,
}, },
where: { where: {
User: {
email: {
not: user.email,
},
},
OR: [ OR: [
{ {
status: ExtendedDocumentStatus.PENDING, status: ExtendedDocumentStatus.PENDING,

View File

@ -0,0 +1,17 @@
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { procedure, router } from '../trpc';
import { ZEncryptSecondaryDataMutationSchema } from './schema';
export const cryptoRouter = router({
encryptSecondaryData: procedure
.input(ZEncryptSecondaryDataMutationSchema)
.mutation(({ input }) => {
try {
return encryptSecondaryData(input);
} catch {
// Never leak errors for crypto.
throw new Error('Failed to encrypt data');
}
}),
});

View File

@ -0,0 +1,15 @@
import { z } from 'zod';
export const ZEncryptSecondaryDataMutationSchema = z.object({
data: z.string(),
expiresAt: z.number().optional(),
});
export const ZDecryptDataMutationSchema = z.object({
data: z.string(),
});
export type TEncryptSecondaryDataMutationSchema = z.infer<
typeof ZEncryptSecondaryDataMutationSchema
>;
export type TDecryptDataMutationSchema = z.infer<typeof ZDecryptDataMutationSchema>;

View File

@ -1,5 +1,6 @@
import { adminRouter } from './admin-router/router'; import { adminRouter } from './admin-router/router';
import { authRouter } from './auth-router/router'; import { authRouter } from './auth-router/router';
import { cryptoRouter } from './crypto/router';
import { documentRouter } from './document-router/router'; import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router'; import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router'; import { profileRouter } from './profile-router/router';
@ -12,6 +13,7 @@ import { twoFactorAuthenticationRouter } from './two-factor-authentication-route
export const appRouter = router({ export const appRouter = router({
auth: authRouter, auth: authRouter,
crypto: cryptoRouter,
profile: profileRouter, profile: profileRouter,
document: documentRouter, document: documentRouter,
field: fieldRouter, field: fieldRouter,

View File

@ -8,6 +8,7 @@ declare namespace NodeJS {
NEXT_PRIVATE_DATABASE_URL: string; NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PRIVATE_ENCRYPTION_KEY: string; NEXT_PRIVATE_ENCRYPTION_KEY: string;
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;

View File

@ -35,7 +35,7 @@ const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps)
<DialogContent className="overflow-hidden p-0 shadow-2xl"> <DialogContent className="overflow-hidden p-0 shadow-2xl">
<Command <Command
{...commandProps} {...commandProps}
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4" className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
> >
{children} {children}
</Command> </Command>
@ -92,7 +92,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium', 'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground mx-2 overflow-hidden border-b pb-2 last:border-0 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:opacity-50 ',
className, className,
)} )}
{...props} {...props}
@ -121,7 +121,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'aria-selected:bg-accent aria-selected:text-accent-foreground relative -mx-2 -my-1 flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className, className,
)} )}
{...props} {...props}

View File

@ -34,6 +34,7 @@
"globalEnv": [ "globalEnv": [
"APP_VERSION", "APP_VERSION",
"NEXT_PRIVATE_ENCRYPTION_KEY", "NEXT_PRIVATE_ENCRYPTION_KEY",
"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY",
"NEXTAUTH_URL", "NEXTAUTH_URL",
"NEXTAUTH_SECRET", "NEXTAUTH_SECRET",
"NEXT_PUBLIC_PROJECT", "NEXT_PUBLIC_PROJECT",