diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..29925fa --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,25 @@ +name: deploy +run-name: Reusable deploy workflow to deploy database changes to the target Supabase database. +on: + workflow_call: + secrets: + DATABASE_URL: + required: true + type: string + DATABASE_DIRECT_URL: + required: true + type: string +jobs: + prisma-migrate-deploy: + name: Run prisma migrations + runs-on: ubuntu-latest + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_DIRECT_URL: ${{ secrets.DATABASE_DIRECT_URL }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + cache: 'npm' + node-version-file: '.nvmrc' + - run: npx prisma@5.0.0 migrate deploy diff --git a/.github/workflows/preview-deploy.yaml b/.github/workflows/preview-deploy.yaml new file mode 100644 index 0000000..e94d41f --- /dev/null +++ b/.github/workflows/preview-deploy.yaml @@ -0,0 +1,16 @@ +name: staging-deploy +run-name: Deploy database changes to Supabase's Staging database. +on: + push: + branches-ignore: + - 'main' +jobs: + call-deploy-workflow: + name: Call reusable deploy workflow + runs-on: ubuntu-latest + environment: + name: staging + uses: ./.github/workflows/deploy.yaml + secrets: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_DIRECT_URL: ${{ secrets.DATABASE_DIRECT_URL }} diff --git a/.github/workflows/production-deploy.yaml b/.github/workflows/production-deploy.yaml new file mode 100644 index 0000000..3eaa525 --- /dev/null +++ b/.github/workflows/production-deploy.yaml @@ -0,0 +1,16 @@ +name: production-deploy +run-name: Deploy database changes to Supabase's Production database. +on: + push: + branches-ignore: + - 'main' +jobs: + call-deploy-workflow: + name: Call reusable deploy workflow + runs-on: ubuntu-latest + environment: + name: production + uses: ./.github/workflows/deploy.yaml + secrets: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_DIRECT_URL: ${{ secrets.DATABASE_DIRECT_URL }} diff --git a/package-lock.json b/package-lock.json index f25c252..978097c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@headlessui/react": "1.7.15", + "@prisma/client": "5.0.0", "@supabase/auth-helpers-nextjs": "0.7.2", "@supabase/supabase-js": "2.26.0", "@tailwindcss/forms": "0.5.3", @@ -34,6 +35,7 @@ "typescript": "5.1.3" }, "devDependencies": { + "prisma": "5.0.0", "supabase": "1.77.9" } }, @@ -403,6 +405,38 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@prisma/client": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.0.0.tgz", + "integrity": "sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + }, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.0.0.tgz", + "integrity": "sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz", + "integrity": "sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==" + }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", @@ -3786,6 +3820,22 @@ } } }, + "node_modules/prisma": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.0.0.tgz", + "integrity": "sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.0.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 313e31f..14212a0 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,14 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "prisma generate && next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "postinstall": "prisma generate" }, "dependencies": { "@headlessui/react": "1.7.15", + "@prisma/client": "5.0.0", "@supabase/auth-helpers-nextjs": "0.7.2", "@supabase/supabase-js": "2.26.0", "@tailwindcss/forms": "0.5.3", @@ -35,6 +37,7 @@ "typescript": "5.1.3" }, "devDependencies": { + "prisma": "5.0.0", "supabase": "1.77.9" } } diff --git a/prisma/migrations/20230721144417_init/migration.sql b/prisma/migrations/20230721144417_init/migration.sql new file mode 100644 index 0000000..0d58197 --- /dev/null +++ b/prisma/migrations/20230721144417_init/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL, + "email" VARCHAR(254) NOT NULL, + "name" VARCHAR(500), + "provider" VARCHAR(100) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_provider_key" ON "User"("provider"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..020869f --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,21 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DATABASE_DIRECT_URL") +} + +generator client { + provider = "prisma-client-js" +} + +// We use Supabase's authentication solution, which create users in a private "auth" schema. +// So we create this User table in our "public" schema and add users to it after +// they're created by Supabase's auth solution, using the same "id". +model User { + id String @id @db.Uuid + email String @unique @db.VarChar(254) + name String? @db.VarChar(500) + provider String @unique @db.VarChar(100) + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt +} diff --git a/src/app/app/shared/user/user-model.ts b/src/app/app/shared/user/user-model.ts new file mode 100644 index 0000000..2e5dacc --- /dev/null +++ b/src/app/app/shared/user/user-model.ts @@ -0,0 +1,24 @@ +import { PrismaClient, User } from '@prisma/client'; + +export interface CreateUserData { + readonly email: string; + readonly id: string; + readonly name?: string; + readonly provider: string; +} + +export const createUser = async (data: CreateUserData) => { + const prisma = new PrismaClient(); + + const user = await prisma.user.findUnique({ where: { id: data.id } }); + + if (user) return user; + + const newUser = await prisma.user.create({ data }); + return newUser; +}; + +export const findUserById = async (id: string) => { + const prisma = new PrismaClient(); + return await prisma.user.findUnique({ where: { id } }); +}; diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts index 303e422..515b89c 100644 --- a/src/app/auth/callback/route.ts +++ b/src/app/auth/callback/route.ts @@ -4,6 +4,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import type { Database } from '@/lib/database.types'; +import { createUser } from '@/app/app/shared/user/user-model'; export async function GET(request: NextRequest) { const requestUrl = new URL(request.url); @@ -12,6 +13,21 @@ export async function GET(request: NextRequest) { if (code) { const supabase = createRouteHandlerClient({ cookies }); await supabase.auth.exchangeCodeForSession(code); + + const { + data: { session }, + } = await supabase.auth.getSession(); + + if (session) { + const { user } = session; + + createUser({ + id: user.id, + email: user.email as string, + name: user.user_metadata.full_name as string, + provider: user.app_metadata.provider as string, + }); + } } // URL to redirect to after sign in process completes diff --git a/src/app/auth/sign-in/check-email-link/page.tsx b/src/app/auth/sign-in/check-email-link/page.tsx index 8db503e..35a7340 100644 --- a/src/app/auth/sign-in/check-email-link/page.tsx +++ b/src/app/auth/sign-in/check-email-link/page.tsx @@ -13,10 +13,8 @@ export default function CheckEmailLink({ searchParams }: { searchParams: { email

Please check your email

-

You're almost there!

-

- We just emailed a link to {getEmailText()}. Click the link, and you'll be signed in. -

+

We just emailed a link to {getEmailText()}.

+

Click on it, and you'll be signed in.

If you don't see it, you may need to{' '} check your spam folder. diff --git a/src/app/auth/sign-in/page.tsx b/src/app/auth/sign-in/page.tsx index f36e809..91f8857 100644 --- a/src/app/auth/sign-in/page.tsx +++ b/src/app/auth/sign-in/page.tsx @@ -20,6 +20,8 @@ interface OAuthProviderButtonProps extends ChildrenProps { } export default function SignIn() { + const redirectTo = `${process.env.NEXT_PUBLIC_VERCEL_URL}/auth/callback`; + const signInWithEmailHandler = async (formData: FormData) => { 'use server'; const email = String(formData.get('email')); @@ -29,7 +31,7 @@ export default function SignIn() { const { data, error } = await supabase.auth.signInWithOtp({ email, options: { - emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + emailRedirectTo: redirectTo, }, }); @@ -50,7 +52,7 @@ export default function SignIn() { const { data, error } = await supabase.auth.signInWithOAuth({ provider, options: { - redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + redirectTo, }, }); diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts index a6ae0b0..f087005 100644 --- a/src/lib/database.types.ts +++ b/src/lib/database.types.ts @@ -34,7 +34,66 @@ export interface Database { } public: { Tables: { - [_ in never]: never + _prisma_migrations: { + Row: { + applied_steps_count: number + checksum: string + finished_at: string | null + id: string + logs: string | null + migration_name: string + rolled_back_at: string | null + started_at: string + } + Insert: { + applied_steps_count?: number + checksum: string + finished_at?: string | null + id: string + logs?: string | null + migration_name: string + rolled_back_at?: string | null + started_at?: string + } + Update: { + applied_steps_count?: number + checksum?: string + finished_at?: string | null + id?: string + logs?: string | null + migration_name?: string + rolled_back_at?: string | null + started_at?: string + } + Relationships: [] + } + User: { + Row: { + createdAt: string + email: string + id: string + name: string | null + provider: string + updatedAt: string | null + } + Insert: { + createdAt?: string + email: string + id: string + name?: string | null + provider: string + updatedAt?: string | null + } + Update: { + createdAt?: string + email?: string + id?: string + name?: string | null + provider?: string + updatedAt?: string | null + } + Relationships: [] + } } Views: { [_ in never]: never diff --git a/src/temp-fix/tailwind-datepicker-react.d.ts b/src/temp-fix/tailwind-datepicker-react.d.ts new file mode 100644 index 0000000..bc23efd --- /dev/null +++ b/src/temp-fix/tailwind-datepicker-react.d.ts @@ -0,0 +1,62 @@ +/* + * Flavio Silva on July 22, 2023: + * Temporary fix. + * Issue: + * https://github.com/OMikkel/tailwind-datepicker-react/issues/19 + * Solution provided here: + * https://github.com/OMikkel/tailwind-datepicker-react/issues/19#issuecomment-1621474204 + */ +declare module 'tailwind-datepicker-react' { + import { ReactElement } from 'react'; + + interface ITheme { + background?: string; + todayBtn?: string; + clearBtn?: string; + icons?: string; + text?: string; + disabledText?: string; + input?: string; + inputIcon?: string; + selected?: string; + } + + interface IIcons { + prev: () => ReactElement; + next: () => ReactElement; + } + + export interface IOptions { + title?: string; + autoHide?: boolean; + todayBtn?: boolean; + todayBtnText?: string; + clearBtn?: boolean; + clearBtnText?: string; + maxDate?: Date; + minDate?: Date; + theme?: ITheme; + icons?: IIcons; + datepickerClassNames?: string; + defaultDate?: Date | null; + language?: string; + weekDays?: string[]; + disabledDates?: Date[]; + inputNameProp?: string; + inputIdProp?: string; + inputPlaceholderProp?: string; + inputDateFormatProp?: Intl.DateTimeFormatOptions; + } + + type DatepickerProps = { + children?: ReactElement | ReactNode; + options?: IOptions; + onChange?: (date: Date) => void; + show: boolean; + setShow: (show: boolean) => void; + classNames?: string; + selectedDateState?: [Date, (date: Date) => void]; + }; + + export default function Datepicker(props: DatepickerProps): ReactElement; +}