diff --git a/.env.example b/.env.example
index 44051e0..73c2cfd 100644
--- a/.env.example
+++ b/.env.example
@@ -3,14 +3,21 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/sos-pet"
# Next Auth
NEXTAUTH_URL="http://localhost:3000"
-NEXTAUTH_SECRET="nextauthsecret"
+NEXTAUTH_SECRET="nextauth-secret"
-# Next Auth Providers
-GOOGLE_CLIENT_ID=""
-GOOGLE_CLIENT_SECRET=""
+# Next Auth Google Sign-In
+GOOGLE_CLIENT_ID="google-client-id"
+GOOGLE_CLIENT_SECRET="google-client-secret"
+# Next Auth Email magic link Sign-In
+# https://resend.com/changelog/smtp-service
+EMAIL_HOST="smtp.resend.com"
+EMAIL_PORT="465"
+EMAIL_USER="resend"
+EMAIL_PASSWORD="resend-api-key"
+EMAIL_FROM="noreply@sos-pet.org"
# Google Maps
-NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=""
+NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="google-maps-api-key"
# Docker related variables, not used on Next.js
POSTGRES_DB="sos-pet"
diff --git a/.github/workflows/lint-type-check-and-build.yaml b/.github/workflows/lint-type-check-and-build.yaml
index 0577526..c6b57b1 100644
--- a/.github/workflows/lint-type-check-and-build.yaml
+++ b/.github/workflows/lint-type-check-and-build.yaml
@@ -9,6 +9,7 @@ on:
jobs:
lint-type-check-and-build:
runs-on: ubuntu-latest
+ name: Lint, Type Check, and Build
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -20,7 +21,10 @@ jobs:
cache: "yarn"
- name: Setup .env file
- run: echo "${{ secrets.DOT_ENV_FILE_CONTENT }}" > .env
+ # All the environment variables in .env.example are invalid,
+ # therefore the application will not be functional. But, it is enough
+ # for linting, type checking, and building.
+ run: cat .env.example > .env
- name: Install dependencies
run: yarn
diff --git a/README.md b/README.md
index 82d961e..d5e12aa 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,6 @@ Com o SOS Pet, as pessoas que resgatam animais de enchentes podem rapidamente en
1. Instale as dependências
1. Crie um arquivo `.env` baseando se no `.env.example`
1. Assegure se de preencher todas as variáveis ambiente.
-1. Suba o baco de dados: `docker compose up -d`
+1. Suba o baco de dados: `docker-compose up -d`
1. Rode as migrations: `npx prisma migrate dev`
1. Rode o projeto com o script `dev` disponível no `package.json`
diff --git a/package.json b/package.json
index 3e07f45..5987d5c 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"next": "^14.2.1",
"next-auth": "^4.24.6",
"next-themes": "^0.3.0",
+ "nodemailer": "^6.9.13",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.4",
@@ -55,6 +56,7 @@
"devDependencies": {
"@types/eslint": "^8.56.2",
"@types/node": "^20.11.20",
+ "@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.1.1",
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 0e50f5b..c07cd8f 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -61,6 +61,8 @@ export default function About() {
transformar um momento de crise em uma oportunidade para fazer a
diferença na vida de um animal. Juntos, podemos salvar vidas e
construir um futuro mais seguro para nossos amigos de quatro patas.
+ Além disso, esse projeto tem o código fonte aberto e disponível para colaboração:
+ https://github.com/emiliosheinz/sos-pet
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3643be0..b99578b 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,7 +2,6 @@
import { Card } from "~/components/card/";
import { SearchInput } from "~/components/search-input";
import { api } from "~/trpc/react";
-import { Filters } from "~/components/filters";
import Fuse from "fuse.js";
import { useMemo } from "react";
import { Skeleton } from "~/components/ui/skeleton";
@@ -10,16 +9,6 @@ import { useDebouncedState } from "~/hooks/use-debouced-state";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { FiInfo } from "react-icons/fi";
-const menus = [
- {
- label: "Disponibilidade",
- items: [
- { label: "Com vagas", checked: true },
- { label: "Sem vagas", checked: false },
- ],
- },
-];
-
export default function Home() {
const { data, isLoading } = api.shelter.findAll.useQuery();
const [searchTerm, setSearchTerm] = useDebouncedState("", 300);
diff --git a/src/app/signin/_components/AuthenticationProviders.tsx b/src/app/signin/_components/AuthenticationProviders.tsx
new file mode 100644
index 0000000..0ca0066
--- /dev/null
+++ b/src/app/signin/_components/AuthenticationProviders.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { getProviders } from "next-auth/react";
+import { useEffect, useMemo, useState } from "react";
+import { SignInProviderButton } from "./SignInProviderButton";
+import { EmailProviderForm } from "./EmailProviderForm";
+import { Loader2 } from "lucide-react";
+import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
+import { FiAlertTriangle } from "react-icons/fi";
+
+type GetProvidersState = "idle" | "loading" | "success" | "error";
+
+type AuthenticationProvidersProps = {
+ callbackUrl?: string;
+};
+
+/**
+ * Prevents multiple calls to getProviders during one session
+ */
+let cachedProviders: Awaited> = null;
+async function getCachedProviders() {
+ if (!!cachedProviders) return cachedProviders;
+ return (cachedProviders = await getProviders());
+}
+
+export function AuthenticationProviders({
+ callbackUrl,
+}: AuthenticationProvidersProps) {
+ const [providers, setProviders] =
+ useState>>();
+ const [getProvidersState, setGetProvidersState] =
+ useState("idle");
+
+ useEffect(() => {
+ setGetProvidersState("loading");
+ getCachedProviders()
+ .then((providers) => {
+ setProviders(providers);
+ setGetProvidersState("success");
+ })
+ .catch(() => {
+ setGetProvidersState("error");
+ });
+ }, []);
+
+ const [emailProvider, otherProviders] = useMemo(() => {
+ if (!providers) return [null, null];
+ const email = providers.email;
+ const other = Object.values(providers).filter(
+ (provider) => provider.id !== "email",
+ );
+ return [email, other];
+ }, [providers]);
+
+ if (["loading", "idle"].includes(getProvidersState)) {
+ return ;
+ }
+
+ if (getProvidersState === "error") {
+ return (
+
+
+ Erro ao carregar provedores de login
+
+ Por favor, entre em contato com o nosso suporte em
+ sospet.suport@gmail.com
+
+
+ );
+ }
+
+ return (
+
+ {otherProviders?.map((provider) => (
+
+ ))}
+ {!!emailProvider && }
+
+ );
+}
diff --git a/src/app/signin/_components/EmailProviderForm.tsx b/src/app/signin/_components/EmailProviderForm.tsx
new file mode 100644
index 0000000..83c3720
--- /dev/null
+++ b/src/app/signin/_components/EmailProviderForm.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Loader2 } from "lucide-react";
+import { signIn } from "next-auth/react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { Button } from "~/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "~/components/ui/form";
+import { Input } from "~/components/ui/input";
+
+const formSchema = z.object({
+ email: z.string().email("Por favor, insira um e-mail válido"),
+});
+
+export function EmailProviderForm() {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: { email: "" },
+ });
+ const [isLoading, setIsLoading] = useState(false);
+
+ const onSubmit = async ({ email }: z.infer) => {
+ setIsLoading(true);
+ await signIn("email", { email });
+ setIsLoading(false);
+ };
+
+ return (
+
+
+ );
+}
diff --git a/src/app/signin/_components/SignInProviderButton.tsx b/src/app/signin/_components/SignInProviderButton.tsx
new file mode 100644
index 0000000..60670c8
--- /dev/null
+++ b/src/app/signin/_components/SignInProviderButton.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { type ClientSafeProvider, signIn } from "next-auth/react";
+import Image from "next/image";
+
+type SignInProviderButtonProps = {
+ provider: ClientSafeProvider;
+ callbackUrl: string;
+};
+
+export function SignInProviderButton({
+ provider,
+ callbackUrl,
+}: SignInProviderButtonProps) {
+ return (
+ signIn(provider.id, { callbackUrl })}
+ >
+
+ Entrar com {provider.name}
+
+ );
+}
diff --git a/src/app/signin/_components/SigninProviderButton.tsx b/src/app/signin/_components/SigninProviderButton.tsx
deleted file mode 100644
index cea7aa3..0000000
--- a/src/app/signin/_components/SigninProviderButton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use client";
-
-import { signIn } from "next-auth/react";
-import Image from "next/image";
-
-type SigninProviderButtonProps = {
- provider: { id: string; name: string };
- callbackUrl: string;
-};
-
-export function SigninProviderButton({
- provider,
- callbackUrl,
-}: SigninProviderButtonProps) {
- return (
- signIn(provider.id, { callbackUrl })}
- >
-
- Entrar com {provider.name}
-
- );
-}
diff --git a/src/app/signin/layout.tsx b/src/app/signin/layout.tsx
new file mode 100644
index 0000000..9f71fc4
--- /dev/null
+++ b/src/app/signin/layout.tsx
@@ -0,0 +1,9 @@
+import { type PropsWithChildren } from "react";
+
+export default function SignInVerifyLayout({ children }: PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx
index ded9df1..3ccace1 100644
--- a/src/app/signin/page.tsx
+++ b/src/app/signin/page.tsx
@@ -1,47 +1,47 @@
-import { getProviders } from "next-auth/react";
import { redirect } from "next/navigation";
import { getServerAuthSession } from "~/server/auth";
-import { SigninProviderButton } from "./_components/SigninProviderButton";
import Image from "next/image";
import { Suspense } from "react";
import { Loader2 } from "lucide-react";
+import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
+import { FiAlertTriangle } from "react-icons/fi";
+import { AuthenticationProviders } from "./_components/AuthenticationProviders";
-type SigninPageProps = {
+type SignInPageProps = {
searchParams: Record;
};
-export default async function SigninPage({ searchParams }: SigninPageProps) {
+export default async function SignInPage({ searchParams }: SignInPageProps) {
const session = await getServerAuthSession();
if (session) {
redirect(searchParams.callbackUrl ?? "/");
}
- const providers = [{ id: "google", name: "Google" }];
+ const hasError = !!searchParams.error;
return (
-
-
}>
-
-
- Em decorrência das enchentes que afetaram o estado do Rio Grande do
- Sul, estamos gerenciando as necessidades de abrigos de animais vítimas
- do desastre.
-
-
- Antes de cadastrar um abrigo você precisa fazer login {" "}
- com uma das opções abaixo:
-
-
- {Object.values(providers).map((provider) => (
-
- ))}
-
-
-
+ }>
+
+
+ Em decorrência das enchentes que afetaram o estado do Rio Grande do Sul,
+ estamos gerenciando as necessidades de abrigos de animais vítimas do
+ desastre.
+
+
+ Antes de cadastrar um abrigo você precisa fazer login com
+ uma das opções abaixo:
+
+ {hasError && (
+
+
+ Erro ao tentar realizar o login.
+
+ Por favor, tente novamente com outra opção disponível.
+
+
+ )}
+
+
);
}
diff --git a/src/app/signin/verify/page.tsx b/src/app/signin/verify/page.tsx
new file mode 100644
index 0000000..ccf79ce
--- /dev/null
+++ b/src/app/signin/verify/page.tsx
@@ -0,0 +1,19 @@
+import { FaRegCheckCircle } from "react-icons/fa";
+
+export default function SignInVerifyPage() {
+ return (
+ <>
+
+
+
+ E-mail enviado com sucesso
+
+
+ Enviamos um e-mail com um link para você verificar sua conta. Por
+ favor, verifique sua caixa de entrada para continuar com o
+ processo de login.
+
+
+ >
+ );
+}
diff --git a/src/components/card/index.tsx b/src/components/card/index.tsx
index 903b2fd..7ec48e6 100644
--- a/src/components/card/index.tsx
+++ b/src/components/card/index.tsx
@@ -22,35 +22,15 @@ export function Card({ shelter }: Props) {
return (
-
-
-
{shelter.name}
-
- {shelter.facebook && (
- }
- label="Facebook"
- />
- )}
-
- {shelter.instagram && (
- }
- label="Instagram"
- />
- )}
-
- {shelter.twitter && (
- }
- label="Twitter"
- />
- )}
-
-
+
+ {shelter.name}
+ {availableVacancies > 0 ? (
+
+ Vagas disponíveis: {availableVacancies}
+
+ ) : (
+ Vagas esgotadas
+ )}
- {availableVacancies > 0 ? (
-
- Vagas disponíveis: {availableVacancies}
-
- ) : (
-
- Vagas esgotadas
-
- )}
+
+ {shelter.facebook && (
+ }
+ label="Facebook"
+ />
+ )}
+
+ {shelter.instagram && (
+ }
+ label="Instagram"
+ />
+ )}
+
+ {shelter.twitter && (
+ }
+ label="Twitter"
+ />
+ )}
+
);
diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx
index 9db2522..0b5862a 100644
--- a/src/components/footer/index.tsx
+++ b/src/components/footer/index.tsx
@@ -1,6 +1,6 @@
import Image from "next/image";
import Link from "next/link";
-import { FaInstagram } from "react-icons/fa";
+import { FaInstagram, FaGithub } from "react-icons/fa";
export function Footer() {
return (
@@ -15,6 +15,13 @@ export function Footer() {
/>
+
+ sos-pet
+
Políticas de privacidade
+
diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx
index e270b43..432dba3 100644
--- a/src/components/header/index.tsx
+++ b/src/components/header/index.tsx
@@ -2,32 +2,41 @@ import Image from "next/image";
import { Nav } from "./nav";
import { Sidebar } from "./sidebar";
import Link from "next/link";
+import { Button } from "../ui/button";
export function Header() {
+ const renderMainSection = () => (
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+ Sobre
+
+
+
+
+ );
+
return (
-
+
-
+ {renderMainSection()}
diff --git a/src/components/header/nav/index.tsx b/src/components/header/nav/index.tsx
index 6ba732c..629c1b6 100644
--- a/src/components/header/nav/index.tsx
+++ b/src/components/header/nav/index.tsx
@@ -1,5 +1,4 @@
"use client";
-import { useRouter } from "next/navigation";
import { Button } from "~/components/ui/button";
import { useSession, signOut } from "next-auth/react";
@@ -14,39 +13,36 @@ import {
import Link from "next/link";
import { User } from "../user";
-import { CiCircleChevDown } from "react-icons/ci";
-
export function Nav() {
const { data: session } = useSession();
- const router = useRouter();
return (
-
+
- router.push("/shelter")} className="mr-2">
- Criar abrigo
+
+ Cadastrar abrigo
{session ? (
-
-
-
+
+
+
+
-
-
- Meu abrigo
-
+
+ Meu abrigo
-
- signOut()}>
- Sair
-
+ signOut()}
+ >
+ Sair
diff --git a/src/components/header/sidebar/index.tsx b/src/components/header/sidebar/index.tsx
index aafecd1..632ba10 100644
--- a/src/components/header/sidebar/index.tsx
+++ b/src/components/header/sidebar/index.tsx
@@ -29,40 +29,56 @@ export function Sidebar() {
return (
-
-
+
+
+
+
-
-
+
+
-
- Criar abrigo
-
+
+ Cadastrar abrigo
+
-
-
- Home
-
+
+
+
+ Home
+
+
-
- Sobre
-
+
+
+ Sobre
+
+
- {session && (
+ {!!session && (
<>
+
-
- Meu abrigo
+
+
+ Meu abrigo
+
+
- signOut()}>
+ {
+ await signOut();
+ handleCloseSidebar();
+ }}
+ >
Sair
-
+
>
)}
diff --git a/src/components/header/user/index.tsx b/src/components/header/user/index.tsx
index 92e5c60..f533203 100644
--- a/src/components/header/user/index.tsx
+++ b/src/components/header/user/index.tsx
@@ -1,67 +1,45 @@
"use client";
import Image from "next/image";
-import { useSession, signIn } from "next-auth/react";
+import { useSession } from "next-auth/react";
-import { Avatar, AvatarImage, AvatarFallback } from "~/components/ui/avatar";
+import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
+import { Button } from "~/components/ui/button";
+import { GoSignIn } from "react-icons/go";
import Link from "next/link";
-const RenderLoginButton = ({
- isLogged,
- children,
-}: {
- isLogged: boolean;
- children: React.ReactNode;
-}) => {
- if (!isLogged) {
- return (
- signIn("google")}>
- {children}
-
- );
- }
-
- return children;
+type UserProps = {
+ onClick?: () => void;
};
-export function User() {
+export function User({ onClick }: UserProps) {
const { data: session } = useSession();
- const renderAvatarImage = () => {
- if (session?.user.image) {
- return ;
- }
-
+ if (!session) {
return (
-
-
-
+
+
+
+ Entrar
+
+
);
- };
-
- const renderUserInfo = () => {
- if (session?.user.name) {
- return `Olá, ${session.user.name}`;
- }
-
- return (
-
- Olá, faça seu login
-
- );
- };
+ }
return (
-
-
-
{renderAvatarImage()}
-
{renderUserInfo()}
-
-
+
+ {session.user.image ? (
+
+ ) : (
+
+
+
+ )}
+
);
}
diff --git a/src/env.js b/src/env.js
index 2124bb8..4ce7941 100644
--- a/src/env.js
+++ b/src/env.js
@@ -24,6 +24,11 @@ export const env = createEnv({
),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
+ EMAIL_HOST: z.string(),
+ EMAIL_PORT: z.string(),
+ EMAIL_USER: z.string(),
+ EMAIL_PASSWORD: z.string(),
+ EMAIL_FROM: z.string(),
},
/**
@@ -46,6 +51,11 @@ export const env = createEnv({
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
+ EMAIL_HOST: process.env.EMAIL_HOST,
+ EMAIL_PORT: process.env.EMAIL_PORT,
+ EMAIL_USER: process.env.EMAIL_USER,
+ EMAIL_PASSWORD: process.env.EMAIL_PASSWORD,
+ EMAIL_FROM: process.env.EMAIL_FROM,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
diff --git a/src/server/auth/EmailProvider.ts b/src/server/auth/EmailProvider.ts
new file mode 100644
index 0000000..f62b9e2
--- /dev/null
+++ b/src/server/auth/EmailProvider.ts
@@ -0,0 +1,97 @@
+import EmailProvider from "next-auth/providers/email";
+import { createTransport } from "nodemailer";
+import { env } from "~/env";
+
+export function CustomEmailProvider() {
+ return EmailProvider({
+ server: {
+ host: env.EMAIL_HOST,
+ port: Number(env.EMAIL_PORT),
+ auth: {
+ user: env.EMAIL_USER,
+ pass: env.EMAIL_PASSWORD,
+ },
+ secure: true,
+ },
+ from: env.EMAIL_FROM,
+ /**
+ * Sends an email with a link to the user to verify their email address.
+ * @see https://next-auth.js.org/providers/email#customizing-emails
+ * */
+ async sendVerificationRequest(params) {
+ const { identifier, url, provider } = params;
+ const transport = createTransport(provider.server);
+ const result = await transport.sendMail({
+ to: identifier,
+ from: provider.from,
+ subject: "Acesse sua conta SOS Pet",
+ text: `Acesse agora a sua conta SOS Pet e comece a ajudar animais resgatados.\n\n`,
+ html: html({ url }),
+ });
+ const failed = result.rejected.concat(result.pending).filter(Boolean);
+ if (failed.length) {
+ throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
+ }
+ },
+ });
+}
+
+function html({ url }: { url: string }) {
+ const color = {
+ background: "#FFFFFF",
+ text: "#333333",
+ mainBackground: "#FFFFFF",
+ buttonBackground: "#333333",
+ buttonBorder: "#333333",
+ buttonText: "#FFFFFF",
+ };
+
+ return `
+
+
+
+
+ Acesse sua conta SOS Pet
+
+
+
+
+
+
+
+ Olá!
+
+ Estamos felizes por você se juntar a nós no SOS Pet, uma comunidade dedicada a conectar animais resgatados de enchentes com abrigos disponíveis. Sua participação é essencial para garantir que esses animais encontrem um lugar seguro e acolhedor.
+
+ Para acessar sua conta no SOS Pet, clique no botão abaixo:
+
+
+
+ Acessar conta
+
+
+
+ Caso o botão acima não funcione, você também pode copiar e colar o link abaixo em seu navegador:
+
+ ${url}
+
+
+
+
+
+
+
+ Se você não solicitou este e-mail, você pode ignorá-lo com segurança.
+
+
+
+
+`;
+}
diff --git a/src/server/auth/GoogleProvider.ts b/src/server/auth/GoogleProvider.ts
new file mode 100644
index 0000000..c70ee2d
--- /dev/null
+++ b/src/server/auth/GoogleProvider.ts
@@ -0,0 +1,9 @@
+import GoogleProvider from "next-auth/providers/google";
+import { env } from "~/env";
+
+export function CustomGoogleProvider() {
+ return GoogleProvider({
+ clientId: env.GOOGLE_CLIENT_ID,
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
+ });
+}
diff --git a/src/server/auth.ts b/src/server/auth/index.ts
similarity index 83%
rename from src/server/auth.ts
rename to src/server/auth/index.ts
index e27b088..22c47f8 100644
--- a/src/server/auth.ts
+++ b/src/server/auth/index.ts
@@ -5,10 +5,9 @@ import {
type NextAuthOptions,
} from "next-auth";
import { type Adapter } from "next-auth/adapters";
-import GoogleProvider from "next-auth/providers/google";
-
-import { env } from "~/env";
import { db } from "~/server/db";
+import { CustomGoogleProvider } from "./GoogleProvider";
+import { CustomEmailProvider } from "./EmailProvider";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -20,15 +19,8 @@ declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
- // ...other properties
- // role: UserRole;
} & DefaultSession["user"];
}
-
- // interface User {
- // // ...other properties
- // // role: UserRole;
- // }
}
/**
@@ -40,9 +32,11 @@ export const authOptions: NextAuthOptions = {
theme: {
colorScheme: "light",
logo: "/logo-horizontal.png",
+ brandColor: "#333333",
},
pages: {
signIn: "/signin",
+ verifyRequest: "/signin/verify",
},
callbacks: {
session: ({ session, user }) => ({
@@ -55,10 +49,8 @@ export const authOptions: NextAuthOptions = {
},
adapter: PrismaAdapter(db) as Adapter,
providers: [
- GoogleProvider({
- clientId: env.GOOGLE_CLIENT_ID,
- clientSecret: env.GOOGLE_CLIENT_SECRET,
- }),
+ CustomGoogleProvider(),
+ CustomEmailProvider(),
/**
* ...add more providers here.
*
diff --git a/yarn.lock b/yarn.lock
index 498398d..b78c083 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -958,9 +958,9 @@
"@babel/runtime" "^7.13.10"
"@rushstack/eslint-patch@^1.3.3":
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz#053f1540703faa81dea2966b768ee5581c66aeda"
- integrity sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
+ integrity sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==
"@sec-ant/readable-stream@^0.4.1":
version "0.4.1"
@@ -1042,51 +1042,51 @@
lodash-es "^4.17.21"
read-pkg-up "^11.0.0"
-"@sigstore/bundle@^2.3.0", "@sigstore/bundle@^2.3.1":
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-2.3.1.tgz#f6cdc67c8400e58ca27f0ef495b27a9327512073"
- integrity sha512-eqV17lO3EIFqCWK3969Rz+J8MYrRZKw9IBHpSo6DEcEX2c+uzDFOgHE9f2MnyDpfs48LFO4hXmk9KhQ74JzU1g==
+"@sigstore/bundle@^2.3.2":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-2.3.2.tgz#ad4dbb95d665405fd4a7a02c8a073dbd01e4e95e"
+ integrity sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==
dependencies:
- "@sigstore/protobuf-specs" "^0.3.1"
+ "@sigstore/protobuf-specs" "^0.3.2"
"@sigstore/core@^1.0.0", "@sigstore/core@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@sigstore/core/-/core-1.1.0.tgz#5583d8f7ffe599fa0a89f2bf289301a5af262380"
integrity sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==
-"@sigstore/protobuf-specs@^0.3.0", "@sigstore/protobuf-specs@^0.3.1":
+"@sigstore/protobuf-specs@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz#5becf88e494a920f548d0163e2978f81b44b7d6f"
integrity sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==
-"@sigstore/sign@^2.3.0":
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-2.3.1.tgz#4fc4e6faee5689b5e9d42e97f1207273b7dd7b7f"
- integrity sha512-YZ71wKIOweC8ViUeZXboz0iPLqMkskxuoeN/D1CEpAyZvEepbX9oRMIoO6a/DxUqO1VEaqmcmmqzSiqtOsvSmw==
+"@sigstore/sign@^2.3.2":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-2.3.2.tgz#d3d01e56d03af96fd5c3a9b9897516b1233fc1c4"
+ integrity sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==
dependencies:
- "@sigstore/bundle" "^2.3.0"
+ "@sigstore/bundle" "^2.3.2"
"@sigstore/core" "^1.0.0"
- "@sigstore/protobuf-specs" "^0.3.1"
+ "@sigstore/protobuf-specs" "^0.3.2"
make-fetch-happen "^13.0.1"
proc-log "^4.2.0"
promise-retry "^2.0.1"
-"@sigstore/tuf@^2.3.1", "@sigstore/tuf@^2.3.3":
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-2.3.3.tgz#be416424d5133b61f1adcc75df72136bf1dfe1ff"
- integrity sha512-agQhHNkIddXFslkudjV88vTXiAMEyUtso3at6ZHUNJ1agZb7Ze6VW/PddHipdWBu1t+8OWLW5X5yZOPiOnaWJQ==
+"@sigstore/tuf@^2.3.3", "@sigstore/tuf@^2.3.4":
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-2.3.4.tgz#da1d2a20144f3b87c0172920cbc8dcc7851ca27c"
+ integrity sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==
dependencies:
- "@sigstore/protobuf-specs" "^0.3.0"
+ "@sigstore/protobuf-specs" "^0.3.2"
tuf-js "^2.2.1"
-"@sigstore/verify@^1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@sigstore/verify/-/verify-1.2.0.tgz#48549186305d8a5e471a3a304cf4cb3e0c99dde7"
- integrity sha512-hQF60nc9yab+Csi4AyoAmilGNfpXT+EXdBgFkP9OgPwIBPwyqVf7JAWPtmqrrrneTmAT6ojv7OlH1f6Ix5BG4Q==
+"@sigstore/verify@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@sigstore/verify/-/verify-1.2.1.tgz#c7e60241b432890dcb8bd8322427f6062ef819e1"
+ integrity sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==
dependencies:
- "@sigstore/bundle" "^2.3.1"
+ "@sigstore/bundle" "^2.3.2"
"@sigstore/core" "^1.1.0"
- "@sigstore/protobuf-specs" "^0.3.1"
+ "@sigstore/protobuf-specs" "^0.3.2"
"@sindresorhus/is@^4.6.0":
version "4.6.0"
@@ -1134,26 +1134,26 @@
integrity sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg==
"@tanstack/react-query@^5.25.0":
- version "5.36.2"
- resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.36.2.tgz#1b7dc4c2fa0e48912335f0a157dd942cfa269326"
- integrity sha512-bHNa+5dead+j6SA8WVlEOPxcGfteVFgdyFTCFcxBgjnPf0fFpHUc7aNZBCnvmPXqy/BeQa9zTuU9ectb7i8ZXA==
+ version "5.37.1"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.37.1.tgz#82d79cdf54923b1f0638c080e8f0439a6a8a2ddc"
+ integrity sha512-EhtBNA8GL3XFeSx6VYUjXQ96n44xe3JGKZCzBINrCYlxbZP6UwBafv7ti4eSRWc2Fy+fybQre0w17gR6lMzULA==
dependencies:
"@tanstack/query-core" "5.36.1"
"@trpc/client@next":
- version "11.0.0-rc.370"
- resolved "https://registry.yarnpkg.com/@trpc/client/-/client-11.0.0-rc.370.tgz#f37d0a97ada1b0d0fbd5d50785ac0379a4921b50"
- integrity sha512-3s3NuJ1wwIod96a2BEXwmCacROfeMnUOhBa9pi/ksQzrr0KkFXeJWsLcHwL8XmZRZ93scPLnX7I7jGTW2ol68w==
+ version "11.0.0-rc.373"
+ resolved "https://registry.yarnpkg.com/@trpc/client/-/client-11.0.0-rc.373.tgz#2bd00e5e9940d54e0b3a26786fb79200afc7c4dd"
+ integrity sha512-DSKOFjpQycJjwLrJWCpMqfwuFCP99QUPBZBeCb94WTvZUb6wGzOZWQMdIj7EnsR9jgWHlPll1e0s7OgAWxBKqw==
"@trpc/react-query@next":
- version "11.0.0-rc.370"
- resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-11.0.0-rc.370.tgz#5c34d6d8db3b0339409bf4ffb4db590930ece40c"
- integrity sha512-DGPJdBsjtshHIh2RLLt+GCEAJaSDoRmJ0hmQF8MGru+7BK5qLlR6N7WFmL4wegs3sV6dov/pyoBXatm0qx3p+g==
+ version "11.0.0-rc.373"
+ resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-11.0.0-rc.373.tgz#67b8fe94eadf6b249c1a30b100641e56a6b13427"
+ integrity sha512-yxHlSiuPkoCnEB0W9yzF5eSX6F09/JFsTpdrwyocTg0Z1TaAYkJ0pUVMP1Gb3bKZEou1x6a8ZJJEqKLZbLPXCA==
"@trpc/server@next":
- version "11.0.0-rc.370"
- resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0-rc.370.tgz#f276f1657d062951510fd0ba56a6e04f9c7ab408"
- integrity sha512-wmAujmM8C21UgSk408436OolK5iOKgLUkMvp1LjON++MRNmZYmFb6JmnjPHn0K9wX0V0qMlME2NGb7v8uTB3lw==
+ version "11.0.0-rc.373"
+ resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0-rc.373.tgz#62cd41f0cda61a37c6616d26fe765de2a66ac648"
+ integrity sha512-i5q1KkQ0fuHqYbEPWMfk2rTp31wde8RKo0jxWfwZU8YsnjaErZ8lFnGCxz252l7mMCqT70IKR6c/N5MEXqChDA==
"@tufjs/canonical-json@2.0.0":
version "2.0.0"
@@ -1196,13 +1196,20 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
-"@types/node@^20.11.20":
+"@types/node@*", "@types/node@^20.11.20":
version "20.12.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050"
integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==
dependencies:
undici-types "~5.26.4"
+"@types/nodemailer@^6.4.15":
+ version "6.4.15"
+ resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.15.tgz#494be695e11c438f7f5df738fb4ab740312a6ed2"
+ integrity sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/normalize-package-data@^2.4.3":
version "2.4.4"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
@@ -1741,9 +1748,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001579:
- version "1.0.30001618"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz#fad74fa006aef0f01e8e5c0a5540c74d8d36ec6f"
- integrity sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==
+ version "1.0.30001620"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz#78bb6f35b8fe315b96b8590597094145d0b146b4"
+ integrity sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==
chalk@^2.3.2, chalk@^2.4.2:
version "2.4.2"
@@ -2795,9 +2802,9 @@ function-bind@^1.1.2:
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
function-timeout@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.1.tgz#fd379f96501e536617a89ae080d44539ed805f63"
- integrity sha512-6yPMImFFuaMPNaTMTBuolA8EanHJWF5Vju0NHpObRURT105J6x1Mf2a7J4P7Sqk2xDxv24N5L0RatEhTBhNmdA==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.2.tgz#e5a7b6ffa523756ff20e1231bbe37b5f373aadd5"
+ integrity sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==
function.prototype.name@^1.1.5, function.prototype.name@^1.1.6:
version "1.1.6"
@@ -4170,6 +4177,11 @@ node-gyp@^10.0.0, node-gyp@^10.1.0:
tar "^6.1.2"
which "^4.0.0"
+nodemailer@^6.9.13:
+ version "6.9.13"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.13.tgz#5b292bf1e92645f4852ca872c56a6ba6c4a3d3d6"
+ integrity sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==
+
nopt@^7.0.0, nopt@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7"
@@ -5326,16 +5338,16 @@ signale@^1.2.1:
pkg-conf "^2.1.0"
sigstore@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-2.3.0.tgz#c56b32818d4dc989f6ea3c0897f4d9bff5d14bed"
- integrity sha512-q+o8L2ebiWD1AxD17eglf1pFrl9jtW7FHa0ygqY6EKvibK8JHyq9Z26v9MZXeDiw+RbfOJ9j2v70M10Hd6E06A==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-2.3.1.tgz#0755dd2cc4820f2e922506da54d3d628e13bfa39"
+ integrity sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==
dependencies:
- "@sigstore/bundle" "^2.3.1"
+ "@sigstore/bundle" "^2.3.2"
"@sigstore/core" "^1.0.0"
- "@sigstore/protobuf-specs" "^0.3.1"
- "@sigstore/sign" "^2.3.0"
- "@sigstore/tuf" "^2.3.1"
- "@sigstore/verify" "^1.2.0"
+ "@sigstore/protobuf-specs" "^0.3.2"
+ "@sigstore/sign" "^2.3.2"
+ "@sigstore/tuf" "^2.3.4"
+ "@sigstore/verify" "^1.2.1"
skin-tone@^2.0.0:
version "2.0.0"