From d5639d87f004731ebfe2986cc064583f39d5e617 Mon Sep 17 00:00:00 2001 From: Jake Holman Date: Wed, 15 Jan 2025 22:31:58 +0000 Subject: [PATCH] feat(cli): adds a new template, one-to-many (#15) * chore(one-to-many): copy of one-to-one * feat(one-to-many): track account membership via join table * chore(one-to-many): remove unused imports * feat(one-to-many): add name field to accounts table * feat(one-to-many): add logic for signing up and setting up a new account * feat(one-to-many): add logic and basic UI for sending invites * feat(one-to-many): display list of users and invites * perf(one-to-many): batch db calls * docs(one-to-many): add jsdocs to actions * fix(one-to-many): remove debugs * fix(one-to-many): some actions were missing use server * fix(one-to-many): add inviterId to invite_tokens * fix(one-to-many): allow removal of users from an account, in addition to removal of invites * fix(one-to-many): perform some integrity checks before removing a user * refactor(one-to-many): abstract repeated action checks into higher order functions * fix(one-to-many): allow changing of user roles * fix(one-to-many): add basic invite page * fix(one-to-many): protect against expiry and existing account * fix(one-to-many): check for invites when signing in a non-account holder user * fix(one-to-many): allow accepting an invite * fix(one-to-many): properly check for empty fetch result * chore(one-to-many): tighten up type checking * chore(one-to-many): linting errors * chore(one-to-many): pnpm update * chore(all): remove lockfiles * fix(one-to-many): drop status column from users_accounts * fix(one-to-many): status is no longer part of users_accounts * refactor(one-to-many): add BASE_URL and cleanup env vars in general * fix(one-to-many): fix env var called for BASE_URL * fix(one-to-many): don't try and write to status * fix(one-to-many): generally improve page styling * fix(one-to-many): move invite email content to component * fix(one-to-many): add account information to invite * fix(one-to-many): send custom magic link emails * chore(one-to-many): such lazy typing * chore(one-to-manu): remove unused * fix(one-to-many): use correct env var * fix(one-to-many): better error messages * docs(one-to-many): add missing jsdocs * fix(one-to-many): redirect to /welcome after invite accept * fix(one-to-many): don't leak email info * fix(one-to-many): add role checks to role changes, deletions, and additions * fix(one-to-many): remove unused import * fix(one-to-many): disable role change select for users * chore(one-to-many): move action to account folder * refactor(one-to-many): simplify fetches when we know we expect only one row result * fix(one-to-many): fix hydration errors * fix(one-to-many): don't clear invite input on validation fails * refactor(one-to-many): abstract invitation page components, and improve UX of buttons * fix(one-to-many): update app welcome message * chore(release): changeset * fix(one-to-many): fix import errors --- .changeset/four-ravens-attend.md | 6 + package.json | 18 +- pnpm-lock.yaml | 692 +- templates/one-to-many/.env.example | 20 + templates/one-to-many/.eslintrc.json | 6 + templates/one-to-many/.gitignore | 37 + templates/one-to-many/README.md | 20 + templates/one-to-many/components.json | 20 + templates/one-to-many/drizzle.config.ts | 10 + templates/one-to-many/next.config.mjs | 4 + templates/one-to-many/package.json | 59 + templates/one-to-many/postcss.config.mjs | 8 + .../one-to-many/public/images/header-bg.png | Bin 0 -> 126651 bytes .../src/actions/account/do-account-setup.ts | 115 + .../actions/account/do-change-user-role.ts | 60 + .../src/actions/account/do-remove-user.ts | 98 + .../fetch-account-users-with-invites.ts | 82 + .../actions/account/fetch-account-users.ts | 37 + .../actions/account/fetch-current-account.ts | 57 + .../src/actions/action-middleware.ts | 174 + .../src/actions/auth/do-magic-auth.ts | 78 + .../src/actions/auth/do-send-magic-link.ts | 39 + .../src/actions/auth/do-signout.ts | 15 + .../src/actions/auth/do-social-auth.ts | 51 + .../src/actions/invite/do-invite-accept.ts | 142 + .../src/actions/invite/do-invite-create.ts | 258 + .../src/actions/invite/do-remove-invite.ts | 75 + .../src/actions/invite/fetch-invite-full.ts | 82 + .../src/actions/invite/fetch-invite.ts | 80 + .../src/actions/user/fetch-current-user.ts | 77 + .../src/actions/user/fetch-user-accounts.ts | 26 + .../one-to-many/src/app/(app)/app/layout.tsx | 66 + .../one-to-many/src/app/(app)/app/page.tsx | 55 + .../src/app/(app)/app/team/page.tsx | 127 + .../src/app/(auth)/invite/[...token]/page.tsx | 56 + .../one-to-many/src/app/(auth)/layout.tsx | 22 + .../src/app/(auth)/signin/page.tsx | 26 + .../src/app/(auth)/signout/page.tsx | 17 + .../src/app/(auth)/signup/page.tsx | 36 + .../src/app/(auth)/verify/page.tsx | 16 + .../src/app/(auth)/welcome/page.tsx | 12 + .../one-to-many/src/app/(site)/layout.tsx | 22 + templates/one-to-many/src/app/(site)/page.tsx | 276 + .../src/app/api/auth/[...nextauth]/route.ts | 2 + templates/one-to-many/src/app/favicon.ico | Bin 0 -> 25931 bytes templates/one-to-many/src/app/globals.css | 96 + .../src/components/accept-invite-button.tsx | 52 + .../src/components/account-setup-form.tsx | 99 + .../src/components/account-signin-form.tsx | 32 + .../one-to-many/src/components/error-card.tsx | 30 + .../src/components/invite-accept-form.tsx | 35 + .../src/components/invite-card.tsx | 70 + .../src/components/invite-user-form.tsx | 112 + .../src/components/layout/app-sidebar.tsx | 107 + .../src/components/layout/email-invite.tsx | 19 + .../components/layout/loading-skeleton.tsx | 48 + .../src/components/layout/nav-main.tsx | 44 + .../src/components/layout/nav-secondary.tsx | 40 + .../src/components/layout/nav-user.tsx | 121 + .../src/components/magic-sign-in-button.tsx | 98 + .../src/components/remove-invite.tsx | 116 + .../src/components/remove-user.tsx | 118 + .../one-to-many/src/components/signout.tsx | 17 + .../src/components/social-sign-in-button.tsx | 48 + .../src/components/svg/google-logo.tsx | 44 + .../src/components/ui/alert-dialog.tsx | 141 + .../one-to-many/src/components/ui/alert.tsx | 59 + .../one-to-many/src/components/ui/avatar.tsx | 50 + .../one-to-many/src/components/ui/badge.tsx | 36 + .../src/components/ui/breadcrumb.tsx | 115 + .../one-to-many/src/components/ui/button.tsx | 57 + .../one-to-many/src/components/ui/card.tsx | 79 + .../src/components/ui/collapsible.tsx | 11 + .../one-to-many/src/components/ui/dialog.tsx | 122 + .../src/components/ui/dropdown-menu.tsx | 200 + .../one-to-many/src/components/ui/input.tsx | 25 + .../one-to-many/src/components/ui/label.tsx | 26 + .../one-to-many/src/components/ui/link.tsx | 27 + .../one-to-many/src/components/ui/select.tsx | 160 + .../src/components/ui/separator.tsx | 31 + .../one-to-many/src/components/ui/sheet.tsx | 140 + .../one-to-many/src/components/ui/sidebar.tsx | 763 +++ .../src/components/ui/sign-in-button.tsx | 31 + .../src/components/ui/skeleton.tsx | 15 + .../one-to-many/src/components/ui/table.tsx | 117 + .../one-to-many/src/components/ui/tooltip.tsx | 30 + .../src/components/user-role-select.tsx | 102 + templates/one-to-many/src/db/index.ts | 5 + .../one-to-many/src/db/schema/accounts.ts | 12 + .../src/db/schema/invite_tokens.ts | 40 + .../one-to-many/src/db/schema/sessions.ts | 10 + templates/one-to-many/src/db/schema/users.ts | 20 + .../src/db/schema/users_accounts.ts | 38 + .../one-to-many/src/db/schema/users_auths.ts | 20 + .../src/db/schema/verification_tokens.ts | 20 + .../hooks/use-form-group-is-submitting.tsx | 41 + .../one-to-many/src/hooks/use-mobile.tsx | 19 + templates/one-to-many/src/lib/auth.ts | 157 + templates/one-to-many/src/lib/schemas.ts | 15 + templates/one-to-many/src/lib/types.ts | 12 + templates/one-to-many/src/lib/utils.ts | 10 + templates/one-to-many/src/middleware.ts | 14 + templates/one-to-many/tailwind.config.ts | 78 + templates/one-to-many/tsconfig.json | 34 + templates/one-to-one/pnpm-lock.yaml | 5928 ----------------- 105 files changed, 7281 insertions(+), 6054 deletions(-) create mode 100644 .changeset/four-ravens-attend.md create mode 100644 templates/one-to-many/.env.example create mode 100644 templates/one-to-many/.eslintrc.json create mode 100644 templates/one-to-many/.gitignore create mode 100644 templates/one-to-many/README.md create mode 100644 templates/one-to-many/components.json create mode 100644 templates/one-to-many/drizzle.config.ts create mode 100644 templates/one-to-many/next.config.mjs create mode 100644 templates/one-to-many/package.json create mode 100644 templates/one-to-many/postcss.config.mjs create mode 100644 templates/one-to-many/public/images/header-bg.png create mode 100644 templates/one-to-many/src/actions/account/do-account-setup.ts create mode 100644 templates/one-to-many/src/actions/account/do-change-user-role.ts create mode 100644 templates/one-to-many/src/actions/account/do-remove-user.ts create mode 100644 templates/one-to-many/src/actions/account/fetch-account-users-with-invites.ts create mode 100644 templates/one-to-many/src/actions/account/fetch-account-users.ts create mode 100644 templates/one-to-many/src/actions/account/fetch-current-account.ts create mode 100644 templates/one-to-many/src/actions/action-middleware.ts create mode 100644 templates/one-to-many/src/actions/auth/do-magic-auth.ts create mode 100644 templates/one-to-many/src/actions/auth/do-send-magic-link.ts create mode 100644 templates/one-to-many/src/actions/auth/do-signout.ts create mode 100644 templates/one-to-many/src/actions/auth/do-social-auth.ts create mode 100644 templates/one-to-many/src/actions/invite/do-invite-accept.ts create mode 100644 templates/one-to-many/src/actions/invite/do-invite-create.ts create mode 100644 templates/one-to-many/src/actions/invite/do-remove-invite.ts create mode 100644 templates/one-to-many/src/actions/invite/fetch-invite-full.ts create mode 100644 templates/one-to-many/src/actions/invite/fetch-invite.ts create mode 100644 templates/one-to-many/src/actions/user/fetch-current-user.ts create mode 100644 templates/one-to-many/src/actions/user/fetch-user-accounts.ts create mode 100644 templates/one-to-many/src/app/(app)/app/layout.tsx create mode 100644 templates/one-to-many/src/app/(app)/app/page.tsx create mode 100644 templates/one-to-many/src/app/(app)/app/team/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/invite/[...token]/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/layout.tsx create mode 100644 templates/one-to-many/src/app/(auth)/signin/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/signout/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/signup/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/verify/page.tsx create mode 100644 templates/one-to-many/src/app/(auth)/welcome/page.tsx create mode 100644 templates/one-to-many/src/app/(site)/layout.tsx create mode 100644 templates/one-to-many/src/app/(site)/page.tsx create mode 100644 templates/one-to-many/src/app/api/auth/[...nextauth]/route.ts create mode 100644 templates/one-to-many/src/app/favicon.ico create mode 100644 templates/one-to-many/src/app/globals.css create mode 100644 templates/one-to-many/src/components/accept-invite-button.tsx create mode 100644 templates/one-to-many/src/components/account-setup-form.tsx create mode 100644 templates/one-to-many/src/components/account-signin-form.tsx create mode 100644 templates/one-to-many/src/components/error-card.tsx create mode 100644 templates/one-to-many/src/components/invite-accept-form.tsx create mode 100644 templates/one-to-many/src/components/invite-card.tsx create mode 100644 templates/one-to-many/src/components/invite-user-form.tsx create mode 100644 templates/one-to-many/src/components/layout/app-sidebar.tsx create mode 100644 templates/one-to-many/src/components/layout/email-invite.tsx create mode 100644 templates/one-to-many/src/components/layout/loading-skeleton.tsx create mode 100644 templates/one-to-many/src/components/layout/nav-main.tsx create mode 100644 templates/one-to-many/src/components/layout/nav-secondary.tsx create mode 100644 templates/one-to-many/src/components/layout/nav-user.tsx create mode 100644 templates/one-to-many/src/components/magic-sign-in-button.tsx create mode 100644 templates/one-to-many/src/components/remove-invite.tsx create mode 100644 templates/one-to-many/src/components/remove-user.tsx create mode 100644 templates/one-to-many/src/components/signout.tsx create mode 100644 templates/one-to-many/src/components/social-sign-in-button.tsx create mode 100644 templates/one-to-many/src/components/svg/google-logo.tsx create mode 100644 templates/one-to-many/src/components/ui/alert-dialog.tsx create mode 100644 templates/one-to-many/src/components/ui/alert.tsx create mode 100644 templates/one-to-many/src/components/ui/avatar.tsx create mode 100644 templates/one-to-many/src/components/ui/badge.tsx create mode 100644 templates/one-to-many/src/components/ui/breadcrumb.tsx create mode 100644 templates/one-to-many/src/components/ui/button.tsx create mode 100644 templates/one-to-many/src/components/ui/card.tsx create mode 100644 templates/one-to-many/src/components/ui/collapsible.tsx create mode 100644 templates/one-to-many/src/components/ui/dialog.tsx create mode 100644 templates/one-to-many/src/components/ui/dropdown-menu.tsx create mode 100644 templates/one-to-many/src/components/ui/input.tsx create mode 100644 templates/one-to-many/src/components/ui/label.tsx create mode 100644 templates/one-to-many/src/components/ui/link.tsx create mode 100644 templates/one-to-many/src/components/ui/select.tsx create mode 100644 templates/one-to-many/src/components/ui/separator.tsx create mode 100644 templates/one-to-many/src/components/ui/sheet.tsx create mode 100644 templates/one-to-many/src/components/ui/sidebar.tsx create mode 100644 templates/one-to-many/src/components/ui/sign-in-button.tsx create mode 100644 templates/one-to-many/src/components/ui/skeleton.tsx create mode 100644 templates/one-to-many/src/components/ui/table.tsx create mode 100644 templates/one-to-many/src/components/ui/tooltip.tsx create mode 100644 templates/one-to-many/src/components/user-role-select.tsx create mode 100644 templates/one-to-many/src/db/index.ts create mode 100644 templates/one-to-many/src/db/schema/accounts.ts create mode 100644 templates/one-to-many/src/db/schema/invite_tokens.ts create mode 100644 templates/one-to-many/src/db/schema/sessions.ts create mode 100644 templates/one-to-many/src/db/schema/users.ts create mode 100644 templates/one-to-many/src/db/schema/users_accounts.ts create mode 100644 templates/one-to-many/src/db/schema/users_auths.ts create mode 100644 templates/one-to-many/src/db/schema/verification_tokens.ts create mode 100644 templates/one-to-many/src/hooks/use-form-group-is-submitting.tsx create mode 100644 templates/one-to-many/src/hooks/use-mobile.tsx create mode 100644 templates/one-to-many/src/lib/auth.ts create mode 100644 templates/one-to-many/src/lib/schemas.ts create mode 100644 templates/one-to-many/src/lib/types.ts create mode 100644 templates/one-to-many/src/lib/utils.ts create mode 100644 templates/one-to-many/src/middleware.ts create mode 100644 templates/one-to-many/tailwind.config.ts create mode 100644 templates/one-to-many/tsconfig.json delete mode 100644 templates/one-to-one/pnpm-lock.yaml diff --git a/.changeset/four-ravens-attend.md b/.changeset/four-ravens-attend.md new file mode 100644 index 0000000..de4f8e6 --- /dev/null +++ b/.changeset/four-ravens-attend.md @@ -0,0 +1,6 @@ +--- +"next-auth-template-one-to-many": minor +"next-auth-template": minor +--- + +Adds one-to-many, a new template that allows multiple users to belong to one account. Also includes invite flows." diff --git a/package.json b/package.json index d1dfb5b..9db5441 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,15 @@ "121:dev": "pnpm --filter next-auth-template-one-to-one run dev", "121:build": "pnpm --filter next-auth-template-one-to-one run build", "121:start": "pnpm --filter next-auth-template-one-to-one run start", - "121:lint": "pnpm --filter next-auth-template-one-to-one run lint" + "121:lint": "pnpm --filter next-auth-template-one-to-one run lint", + "12m:db:push": "pnpm --filter next-auth-template-one-to-many run db:push", + "12m:db:studio": "pnpm --filter next-auth-template-one-to-many run db:studio", + "12m:db:migrate": "pnpm --filter next-auth-template-one-to-many run db:migrate", + "12m:db:generate": "pnpm --filter next-auth-template-one-to-many run db:generate", + "12m:dev": "pnpm --filter next-auth-template-one-to-many run dev", + "12m:build": "pnpm --filter next-auth-template-one-to-many run build", + "12m:start": "pnpm --filter next-auth-template-one-to-many run start", + "12m:lint": "pnpm --filter next-auth-template-one-to-many run lint" }, "bin": "./dist/index.js", "files": [ @@ -54,12 +62,12 @@ }, "devDependencies": { "@types/fs-extra": "^11.0.4", - "@types/node": "^22.10.3", + "@types/node": "^22.10.5", "@types/prompts": "^2.4.9", - "eslint": "^8", + "eslint": "^8.57.1", "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.8", + "prettier-plugin-tailwindcss": "^0.6.9", "tsup": "^8.3.5", - "typescript": "^5.7.2" + "typescript": "^5.7.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ba8458..a79efdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,25 +34,143 @@ importers: specifier: ^11.0.4 version: 11.0.4 '@types/node': - specifier: ^22.10.3 + specifier: ^22.10.5 version: 22.10.5 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 eslint: - specifier: ^8 + specifier: ^8.57.1 version: 8.57.1 prettier: specifier: ^3.4.2 version: 3.4.2 prettier-plugin-tailwindcss: - specifier: ^0.6.8 + specifier: ^0.6.9 version: 0.6.9(@trivago/prettier-plugin-sort-imports@5.2.1(prettier@3.4.2))(prettier@3.4.2) tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.7)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(jiti@1.21.7)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) typescript: - specifier: ^5.7.2 + specifier: ^5.7.3 + version: 5.7.3 + + templates/one-to-many: + dependencies: + '@auth/drizzle-adapter': + specifier: ^1.7.4 + version: 1.7.4 + '@neondatabase/serverless': + specifier: ^0.10.3 + version: 0.10.4 + '@radix-ui/react-alert-dialog': + specifier: ^1.1.4 + version: 1.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-avatar': + specifier: ^1.1.2 + version: 1.1.2(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.2 + version: 1.1.2(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-dialog': + specifier: ^1.1.2 + version: 1.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-select': + specifier: ^2.1.4 + version: 2.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-separator': + specifier: ^1.1.1 + version: 1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-slot': + specifier: ^1.1.1 + version: 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.6 + version: 1.1.6(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@types/react': + specifier: npm:types-react@rc + version: types-react@19.0.0-rc.1 + '@types/react-dom': + specifier: npm:types-react-dom@rc + version: types-react-dom@19.0.0-rc.1 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + drizzle-orm: + specifier: ^0.38.3 + version: 0.38.3(@neondatabase/serverless@0.10.4)(@types/pg@8.11.10)(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + lucide-react: + specifier: ^0.451.0 + version: 0.451.0(react@19.0.0-rc.1) + next: + specifier: 15.1.3 + version: 15.1.3(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) + next-auth: + specifier: 5.0.0-beta.25 + version: 5.0.0-beta.25(next@15.1.3(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1))(react@19.0.0-rc.1) + prettier: + specifier: ^3.3.3 + version: 3.4.2 + prettier-plugin-tailwindcss: + specifier: ^0.6.8 + version: 0.6.9(@trivago/prettier-plugin-sort-imports@5.2.1(prettier@3.4.2))(prettier@3.4.2) + react: + specifier: 19.0.0-rc.1 + version: 19.0.0-rc.1 + react-dom: + specifier: 19.0.0-rc.1 + version: 19.0.0-rc.1(react@19.0.0-rc.1) + resend: + specifier: ^4.0.1 + version: 4.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) + tailwind-merge: + specifier: ^2.5.3 + version: 2.6.0 + zod: + specifier: ^3.24.1 + version: 3.24.1 + devDependencies: + '@types/node': + specifier: ^20 + version: 20.17.11 + '@types/pg': + specifier: ^8.11.10 + version: 8.11.10 + drizzle-kit: + specifier: ^0.30.1 + version: 0.30.1 + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 15.0.3 + version: 15.0.3(eslint@8.57.1)(typescript@5.7.2) + postcss: + specifier: ^8 + version: 8.4.49 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.17 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) + tsx: + specifier: ^4.19.1 + version: 4.19.2 + typescript: + specifier: ^5 version: 5.7.2 templates/one-to-one: @@ -202,8 +320,8 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.26.3': - resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + '@babel/generator@7.26.5': + resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.25.9': @@ -214,8 +332,8 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.3': - resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + '@babel/parser@7.26.5': + resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} engines: {node: '>=6.0.0'} hasBin: true @@ -227,12 +345,12 @@ packages: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.4': - resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + '@babel/traverse@7.26.5': + resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.3': - resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + '@babel/types@7.26.5': + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} engines: {node: '>=6.9.0'} '@changesets/apply-release-plan@7.0.7': @@ -1126,6 +1244,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@panva/hkdf@1.2.1': resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} @@ -1133,9 +1254,25 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-alert-dialog@1.1.4': + resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.1': resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} peerDependencies: @@ -1376,6 +1513,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.4': + resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-separator@1.1.1': resolution: {integrity: sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==} peerDependencies: @@ -1447,6 +1597,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -1481,98 +1640,105 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} - '@rollup/rollup-android-arm-eabi@4.29.1': - resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==} + '@react-email/render@1.0.1': + resolution: {integrity: sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@rollup/rollup-android-arm-eabi@4.30.1': + resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.29.1': - resolution: {integrity: sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==} + '@rollup/rollup-android-arm64@4.30.1': + resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.29.1': - resolution: {integrity: sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==} + '@rollup/rollup-darwin-arm64@4.30.1': + resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.29.1': - resolution: {integrity: sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==} + '@rollup/rollup-darwin-x64@4.30.1': + resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.29.1': - resolution: {integrity: sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==} + '@rollup/rollup-freebsd-arm64@4.30.1': + resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.29.1': - resolution: {integrity: sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==} + '@rollup/rollup-freebsd-x64@4.30.1': + resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.29.1': - resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==} + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.29.1': - resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==} + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.29.1': - resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==} + '@rollup/rollup-linux-arm64-gnu@4.30.1': + resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.29.1': - resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==} + '@rollup/rollup-linux-arm64-musl@4.30.1': + resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.29.1': - resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==} + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': - resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==} + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.29.1': - resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==} + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.29.1': - resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==} + '@rollup/rollup-linux-s390x-gnu@4.30.1': + resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.29.1': - resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==} + '@rollup/rollup-linux-x64-gnu@4.30.1': + resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.29.1': - resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==} + '@rollup/rollup-linux-x64-musl@4.30.1': + resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.29.1': - resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==} + '@rollup/rollup-win32-arm64-msvc@4.30.1': + resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.29.1': - resolution: {integrity: sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==} + '@rollup/rollup-win32-ia32-msvc@4.30.1': + resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.29.1': - resolution: {integrity: sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==} + '@rollup/rollup-win32-x64-msvc@4.30.1': + resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==} cpu: [x64] os: [win32] @@ -1582,6 +1748,9 @@ packages: '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1640,6 +1809,9 @@ packages: '@types/react@19.0.2': resolution: {integrity: sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@typescript-eslint/eslint-plugin@8.19.0': resolution: {integrity: sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1690,6 +1862,10 @@ packages: '@ungap/structured-clone@1.2.1': resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1914,6 +2090,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@13.0.0: resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} engines: {node: '>=18'} @@ -1925,6 +2105,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.3.3: resolution: {integrity: sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1983,6 +2166,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2020,6 +2207,19 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -2127,6 +2327,11 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2144,6 +2349,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-abstract@1.23.9: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} @@ -2328,6 +2537,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2339,6 +2551,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2508,6 +2724,13 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -2534,6 +2757,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -2699,6 +2925,15 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2752,6 +2987,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2822,6 +3060,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2897,6 +3139,11 @@ packages: encoding: optional: true + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3007,6 +3254,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3030,6 +3280,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -3249,6 +3502,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3264,6 +3520,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -3309,9 +3568,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readdirp@4.0.2: - resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} - engines: {node: '>= 14.16.0'} + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} @@ -3324,6 +3583,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + resend@4.0.1: + resolution: {integrity: sha512-EkCRfzKw9JX7N75L+0BC8oXohDBLhlhl4w7AgrkEW2TAsOMBsVcbQHPe8cRWP6Ea7KDhD158TsNjbCBcohed5A==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3357,8 +3620,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.29.1: - resolution: {integrity: sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==} + rollup@4.30.1: + resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3383,6 +3646,9 @@ packages: scheduler@0.25.0-rc.1: resolution: {integrity: sha512-fVinv2lXqYpKConAMdergOl5owd0rY1O4P/QTe0aWKCqGtu7VsCt1iqQFxSJtqK4Lci/upVSBpGwVC7eWcuS9Q==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3685,6 +3951,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3826,10 +4097,10 @@ snapshots: picocolors: 1.1.1 optional: true - '@babel/generator@7.26.3': + '@babel/generator@7.26.5': dependencies: - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 @@ -3841,9 +4112,9 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': optional: true - '@babel/parser@7.26.3': + '@babel/parser@7.26.5': dependencies: - '@babel/types': 7.26.3 + '@babel/types': 7.26.5 optional: true '@babel/runtime@7.26.0': @@ -3853,24 +4124,24 @@ snapshots: '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 optional: true - '@babel/traverse@7.26.4': + '@babel/traverse@7.26.5': dependencies: '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.3 - '@babel/parser': 7.26.3 + '@babel/generator': 7.26.5 + '@babel/parser': 7.26.5 '@babel/template': 7.25.9 - '@babel/types': 7.26.3 + '@babel/types': 7.26.5 debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color optional: true - '@babel/types@7.26.3': + '@babel/types@7.26.5': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 @@ -4549,13 +4820,31 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@one-ini/wasm@0.1.1': {} + '@panva/hkdf@1.2.1': {} '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-alert-dialog@1.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-context': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-dialog': 1.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-slot': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + react: 19.0.0-rc.1 + react-dom: 19.0.0-rc.1(react@19.0.0-rc.1) + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-arrow@1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -4796,6 +5085,35 @@ snapshots: '@types/react': types-react@19.0.0-rc.1 '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-select@2.1.4(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-context': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-direction': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-dismissable-layer': 1.1.3(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-focus-guards': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-focus-scope': 1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-id': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-popper': 1.2.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-portal': 1.1.3(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-slot': 1.1.1(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-use-controllable-state': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-use-previous': 1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-visually-hidden': 1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + aria-hidden: 1.2.4 + react: 19.0.0-rc.1 + react-dom: 19.0.0-rc.1(react@19.0.0-rc.1) + react-remove-scroll: 2.6.2(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-separator@1.1.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -4858,6 +5176,12 @@ snapshots: optionalDependencies: '@types/react': types-react@19.0.0-rc.1 + '@radix-ui/react-use-previous@1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + dependencies: + react: 19.0.0-rc.1 + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + '@radix-ui/react-use-rect@1.1.0(react@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: '@radix-ui/rect': 1.1.0 @@ -4883,67 +5207,80 @@ snapshots: '@radix-ui/rect@1.1.0': {} - '@rollup/rollup-android-arm-eabi@4.29.1': + '@react-email/render@1.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)': + dependencies: + html-to-text: 9.0.5 + js-beautify: 1.15.1 + react: 19.0.0-rc.1 + react-dom: 19.0.0-rc.1(react@19.0.0-rc.1) + react-promise-suspense: 0.3.4 + + '@rollup/rollup-android-arm-eabi@4.30.1': optional: true - '@rollup/rollup-android-arm64@4.29.1': + '@rollup/rollup-android-arm64@4.30.1': optional: true - '@rollup/rollup-darwin-arm64@4.29.1': + '@rollup/rollup-darwin-arm64@4.30.1': optional: true - '@rollup/rollup-darwin-x64@4.29.1': + '@rollup/rollup-darwin-x64@4.30.1': optional: true - '@rollup/rollup-freebsd-arm64@4.29.1': + '@rollup/rollup-freebsd-arm64@4.30.1': optional: true - '@rollup/rollup-freebsd-x64@4.29.1': + '@rollup/rollup-freebsd-x64@4.30.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.29.1': + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.29.1': + '@rollup/rollup-linux-arm-musleabihf@4.30.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.29.1': + '@rollup/rollup-linux-arm64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.29.1': + '@rollup/rollup-linux-arm64-musl@4.30.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.29.1': + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.29.1': + '@rollup/rollup-linux-riscv64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.29.1': + '@rollup/rollup-linux-s390x-gnu@4.30.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.29.1': + '@rollup/rollup-linux-x64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-x64-musl@4.29.1': + '@rollup/rollup-linux-x64-musl@4.30.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.29.1': + '@rollup/rollup-win32-arm64-msvc@4.30.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.29.1': + '@rollup/rollup-win32-ia32-msvc@4.30.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.29.1': + '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.4': {} + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -4952,10 +5289,10 @@ snapshots: '@trivago/prettier-plugin-sort-imports@5.2.1(prettier@3.4.2)': dependencies: - '@babel/generator': 7.26.3 - '@babel/parser': 7.26.3 - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.3 + '@babel/generator': 7.26.5 + '@babel/parser': 7.26.5 + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 javascript-natural-sort: 0.7.1 lodash: 4.17.21 prettier: 3.4.2 @@ -4990,13 +5327,13 @@ snapshots: '@types/pg@8.11.10': dependencies: - '@types/node': 20.17.11 + '@types/node': 22.10.5 pg-protocol: 1.7.0 pg-types: 4.0.2 '@types/pg@8.11.6': dependencies: - '@types/node': 20.17.11 + '@types/node': 22.10.5 pg-protocol: 1.7.0 pg-types: 4.0.2 @@ -5009,6 +5346,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/uuid@10.0.0': {} + '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -5088,6 +5427,8 @@ snapshots: '@ungap/structured-clone@1.2.1': {} + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -5290,7 +5631,7 @@ snapshots: chokidar@4.0.3: dependencies: - readdirp: 4.0.2 + readdirp: 4.1.1 ci-info@3.9.0: {} @@ -5326,12 +5667,19 @@ snapshots: color-string: 1.9.1 optional: true + commander@10.0.1: {} + commander@13.0.0: {} commander@4.1.1: {} concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + consola@3.3.3: {} cookie@0.7.1: {} @@ -5378,6 +5726,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -5413,6 +5763,24 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@8.6.0: {} drizzle-kit@0.30.1: @@ -5439,6 +5807,13 @@ snapshots: eastasianwidth@0.2.0: {} + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.3 + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -5455,6 +5830,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + es-abstract@1.23.9: dependencies: array-buffer-byte-length: 1.0.2 @@ -5712,7 +6089,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -5734,7 +6111,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5877,6 +6254,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -5895,6 +6274,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -6052,7 +6439,7 @@ snapshots: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 @@ -6085,6 +6472,21 @@ snapshots: dependencies: function-bind: 1.1.2 + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + human-id@1.0.2: {} iconv-lite@0.4.24: @@ -6107,6 +6509,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -6272,6 +6676,16 @@ snapshots: joycon@3.1.1: {} + js-beautify@1.15.1: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -6325,6 +6739,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -6383,6 +6799,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -6440,6 +6860,10 @@ snapshots: dependencies: whatwg-url: 5.0.0 + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-path@3.0.0: {} oauth4webapi@3.1.4: {} @@ -6560,6 +6984,11 @@ snapshots: dependencies: callsites: 3.1.0 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -6575,6 +7004,8 @@ snapshots: path-type@4.0.0: {} + peberminta@0.9.0: {} + pg-int8@1.0.1: {} pg-numeric@1.0.2: {} @@ -6707,6 +7138,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proto-list@1.2.4: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -6718,6 +7151,10 @@ snapshots: react-is@16.13.1: {} + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + react-remove-scroll-bar@2.3.8(react@19.0.0-rc.1)(types-react@19.0.0-rc.1): dependencies: react: 19.0.0-rc.1 @@ -6762,7 +7199,7 @@ snapshots: dependencies: picomatch: 2.3.1 - readdirp@4.0.2: {} + readdirp@4.1.1: {} reflect.getprototypeof@1.0.10: dependencies: @@ -6786,6 +7223,13 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + resend@4.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1): + dependencies: + '@react-email/render': 1.0.1(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) + transitivePeerDependencies: + - react + - react-dom + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -6815,29 +7259,29 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.29.1: + rollup@4.30.1: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.29.1 - '@rollup/rollup-android-arm64': 4.29.1 - '@rollup/rollup-darwin-arm64': 4.29.1 - '@rollup/rollup-darwin-x64': 4.29.1 - '@rollup/rollup-freebsd-arm64': 4.29.1 - '@rollup/rollup-freebsd-x64': 4.29.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.29.1 - '@rollup/rollup-linux-arm-musleabihf': 4.29.1 - '@rollup/rollup-linux-arm64-gnu': 4.29.1 - '@rollup/rollup-linux-arm64-musl': 4.29.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.29.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.29.1 - '@rollup/rollup-linux-riscv64-gnu': 4.29.1 - '@rollup/rollup-linux-s390x-gnu': 4.29.1 - '@rollup/rollup-linux-x64-gnu': 4.29.1 - '@rollup/rollup-linux-x64-musl': 4.29.1 - '@rollup/rollup-win32-arm64-msvc': 4.29.1 - '@rollup/rollup-win32-ia32-msvc': 4.29.1 - '@rollup/rollup-win32-x64-msvc': 4.29.1 + '@rollup/rollup-android-arm-eabi': 4.30.1 + '@rollup/rollup-android-arm64': 4.30.1 + '@rollup/rollup-darwin-arm64': 4.30.1 + '@rollup/rollup-darwin-x64': 4.30.1 + '@rollup/rollup-freebsd-arm64': 4.30.1 + '@rollup/rollup-freebsd-x64': 4.30.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.1 + '@rollup/rollup-linux-arm-musleabihf': 4.30.1 + '@rollup/rollup-linux-arm64-gnu': 4.30.1 + '@rollup/rollup-linux-arm64-musl': 4.30.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1 + '@rollup/rollup-linux-riscv64-gnu': 4.30.1 + '@rollup/rollup-linux-s390x-gnu': 4.30.1 + '@rollup/rollup-linux-x64-gnu': 4.30.1 + '@rollup/rollup-linux-x64-musl': 4.30.1 + '@rollup/rollup-win32-arm64-msvc': 4.30.1 + '@rollup/rollup-win32-ia32-msvc': 4.30.1 + '@rollup/rollup-win32-x64-msvc': 4.30.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -6867,6 +7311,10 @@ snapshots: scheduler@0.25.0-rc.1: {} + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@6.3.1: {} semver@7.6.3: {} @@ -7177,7 +7625,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(jiti@1.21.7)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.7.0): + tsup@8.3.5(jiti@1.21.7)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0): dependencies: bundle-require: 5.1.0(esbuild@0.24.2) cac: 6.7.14 @@ -7189,7 +7637,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.4.49)(tsx@4.19.2)(yaml@2.7.0) resolve-from: 5.0.0 - rollup: 4.29.1 + rollup: 4.30.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 @@ -7197,7 +7645,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.4.49 - typescript: 5.7.2 + typescript: 5.7.3 transitivePeerDependencies: - jiti - supports-color @@ -7260,6 +7708,8 @@ snapshots: typescript@5.7.2: {} + typescript@5.7.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 diff --git a/templates/one-to-many/.env.example b/templates/one-to-many/.env.example new file mode 100644 index 0000000..e91de3a --- /dev/null +++ b/templates/one-to-many/.env.example @@ -0,0 +1,20 @@ +# The base URL of the app. Required. +BASE_URL="" + +# The database connection string. Required. https://orm.drizzle.team/docs/connect-overview +DATABASE_URL="" + +# The API key for Resend. Required for magic links. If not set, disables magic link auth. https://resend.com/ https://authjs.dev/guides/configuring-resend +RESEND_KEY="" + +# The email address to send magic links from. Required for magic links. +RESEND_EMAIL_FROM="" + +# A secret used to sign cookies and to sign and verify JSON Web Tokens. See Auth.js docs on how to generate. Required in production. https://authjs.dev/getting-started/deployment#auth_secret +AUTH_SECRET="" + +# The Client ID for your Google OAuth app. Required for social sign in. See Auth.js docs for set up. https://authjs.dev/getting-started/providers/google#setup +AUTH_GOOGLE_ID="" + +# The Client Secret for your Google OAuth app. Required for social sign in. See Auth.js docs for set up. https://authjs.dev/getting-started/providers/google#setup +AUTH_GOOGLE_SECRET="" diff --git a/templates/one-to-many/.eslintrc.json b/templates/one-to-many/.eslintrc.json new file mode 100644 index 0000000..e0fedbd --- /dev/null +++ b/templates/one-to-many/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-empty-object-type": "off" + } +} diff --git a/templates/one-to-many/.gitignore b/templates/one-to-many/.gitignore new file mode 100644 index 0000000..00bba9b --- /dev/null +++ b/templates/one-to-many/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/templates/one-to-many/README.md b/templates/one-to-many/README.md new file mode 100644 index 0000000..2c1af0a --- /dev/null +++ b/templates/one-to-many/README.md @@ -0,0 +1,20 @@ +# next-auth-template + +![Built on next.js](https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/next?style=flat-square) ![Built on next-auth](https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/next-auth?style=flat-square) ![Built on drizzle-orm](https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/drizzle-orm?style=flat-square) ![Built on zod](https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/zod?style=flat-square) + +This template gets you up a running with social sign in, magic links, database-backed sessions, and account creation and setup. It's a good starting point for your next project. + +### [[View the demo]](https://next-auth-template-demo.vercel.app/) + +_\* The demo database is reset every few hours_ + +> [!NOTE] +> This template uses major dependencies that are not yet stable. It is not recommended for production use until `next-auth` and `drizzle-orm` are stable + +# Documentation + +Visit https://jakeisonline.com/playground/tools/next-auth-template for detailed documentation. + +# License + +Licensed under the [MIT license](./LICENSE). diff --git a/templates/one-to-many/components.json b/templates/one-to-many/components.json new file mode 100644 index 0000000..792124c --- /dev/null +++ b/templates/one-to-many/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/templates/one-to-many/drizzle.config.ts b/templates/one-to-many/drizzle.config.ts new file mode 100644 index 0000000..aa4f8e3 --- /dev/null +++ b/templates/one-to-many/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + schema: "./src/db/schema", + out: "./src/db/migrations", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}) diff --git a/templates/one-to-many/next.config.mjs b/templates/one-to-many/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/templates/one-to-many/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/templates/one-to-many/package.json b/templates/one-to-many/package.json new file mode 100644 index 0000000..196d49f --- /dev/null +++ b/templates/one-to-many/package.json @@ -0,0 +1,59 @@ +{ + "name": "next-auth-template-one-to-many", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:migrate": "drizzle-kit migrate", + "db:generate": "drizzle-kit generate" + }, + "dependencies": { + "@auth/drizzle-adapter": "^1.7.4", + "@neondatabase/serverless": "^0.10.3", + "@radix-ui/react-alert-dialog": "^1.1.4", + "@radix-ui/react-avatar": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.6", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "@types/uuid": "^10.0.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "drizzle-orm": "^0.38.3", + "lucide-react": "^0.451.0", + "next": "15.1.3", + "next-auth": "5.0.0-beta.25", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", + "react": "19.0.0-rc.1", + "react-dom": "19.0.0-rc.1", + "resend": "^4.0.1", + "tailwind-merge": "^2.5.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^20", + "@types/pg": "^8.11.10", + "@types/react": "^18", + "@types/react-dom": "^18", + "drizzle-kit": "^0.30.1", + "eslint": "^8", + "eslint-config-next": "15.0.3", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "tailwindcss-animate": "^1.0.7", + "tsx": "^4.19.1", + "typescript": "^5" + } +} diff --git a/templates/one-to-many/postcss.config.mjs b/templates/one-to-many/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/templates/one-to-many/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/templates/one-to-many/public/images/header-bg.png b/templates/one-to-many/public/images/header-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..9fecc3e84e781d3abd30c0b016d912f07acaa284 GIT binary patch literal 126651 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 literal 0 HcmV?d00001 diff --git a/templates/one-to-many/src/actions/account/do-account-setup.ts b/templates/one-to-many/src/actions/account/do-account-setup.ts new file mode 100644 index 0000000..e2a6496 --- /dev/null +++ b/templates/one-to-many/src/actions/account/do-account-setup.ts @@ -0,0 +1,115 @@ +"use server" + +import { db } from "@/db" +import { accountsTable } from "@/db/schema/accounts" +import { z } from "zod" +import { ServerActionResponse } from "@/lib/types" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { withFormProtection } from "@/actions/action-middleware" + +/** + * Server action to set up a new account for a authenticated user. + * + * @param prevState - Previous server action response state (unused but required for Next.js Server Actions) + * @param formData - Form data containing the account setup information + * @returns {Promise} Response object containing status and optional messages + * + * Protected by withActionProtection middleware which ensures: + * - User is authenticated + * - FormData is present + * + * The function performs the following: + * 1. Validates the account name + * 2. Creates a new account in the database + * 3. Links the account to the authenticated user as owner + */ + +export const doAccountSetup = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const validatedAccountName = z + .string({ + required_error: "required_error", + invalid_type_error: "invalid_type_error", + }) + .trim() + .safeParse(formData!.get("account_name")) + + if (!validatedAccountName.success) { + return { + status: "error", + messages: [ + { + title: "Invalid account name", + body: "No account name was provided, or type is invalid", + }, + ], + } + } + + try { + const createdAccount = await db + .insert(accountsTable) + .values({ + name: validatedAccountName.data, + }) + .returning() + + if (!createdAccount[0].id) { + return { + status: "error", + messages: [ + { + title: "Account creation failed", + body: "Failed to create account", + }, + ], + } + } + + const createdUserAccount = await db + .insert(usersAccountsTable) + .values({ + userId: formData!.get("sessionUserId") as string, + accountId: createdAccount[0].id, + role: "owner", + }) + .returning() + + if (!createdUserAccount[0].id) { + return { + status: "error", + messages: [ + { + title: "Account setup failed", + body: "Failed to update user account", + }, + ], + } + } + + return { + status: "success", + } + } catch (error) { + return { + status: "error", + messages: [ + { + title: "Account setup failed", + body: + error instanceof Error + ? error.message + : "An unknown error occurred", + }, + ], + } + } + }, + { + requireAuth: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/account/do-change-user-role.ts b/templates/one-to-many/src/actions/account/do-change-user-role.ts new file mode 100644 index 0000000..0227910 --- /dev/null +++ b/templates/one-to-many/src/actions/account/do-change-user-role.ts @@ -0,0 +1,60 @@ +"use server" + +import { db } from "@/db" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { ServerActionResponse } from "@/lib/types" +import { eq } from "drizzle-orm" +import { withFormProtection } from "@/actions/action-middleware" +import { userAccountsRoles } from "@/db/schema/users_accounts" +import { z } from "zod" +import { revalidatePath } from "next/cache" + +export const doChangeUserRole = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const userId = z.string().uuid().safeParse(formData!.get("userId")) + const role = z + .enum(userAccountsRoles.enumValues) + .safeParse(formData!.get("role")) + + if (!userId.success || !role.success) { + return { + status: "error", + messages: [ + { + title: "Invalid userId or role", + body: "The userId is not a valid UUID or role is not a valid role", + }, + ], + } + } + + const result = await db + .update(usersAccountsTable) + .set({ role: role.data }) + .where(eq(usersAccountsTable.userId, userId.data)) + + if (result.rowCount === 0) { + return { + status: "error", + messages: [{ title: "User not found", body: "The user was not found" }], + } + } + + revalidatePath("/app/team") + + return { + status: "success", + data: { + role: role.data, + }, + } + }, + { + requireAuth: true, + requireAdmin: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/account/do-remove-user.ts b/templates/one-to-many/src/actions/account/do-remove-user.ts new file mode 100644 index 0000000..5ee13d3 --- /dev/null +++ b/templates/one-to-many/src/actions/account/do-remove-user.ts @@ -0,0 +1,98 @@ +"use server" + +import { db } from "@/db" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { and, eq, not } from "drizzle-orm" +import { revalidatePath } from "next/cache" +import { z } from "zod" +import { withFormProtection } from "@/actions/action-middleware" +import { ServerActionResponse } from "@/lib/types" + +/** + * Removes a user from an account + * + * @param prevState - Previous server action response state (unused but required for Next.js Server Actions) + * @param formData - Form data containing userId and accountId + * @returns {Promise} Response object containing status and optional messages + * + * Protected by withActionProtection middleware which ensures: + * - User is authenticated + * - User has admin permissions + * - FormData is present + * + * The function performs the following checks: + * 1. Validates the user and account IDs + * 2. Prevents self-removal + * 3. Prevents removal of account owner + * 4. Removes the user-account association + * 5. Revalidates the team page cache + */ + +export const doRemoveUser = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const userId = z.string().uuid().safeParse(formData!.get("userId")) + const accountId = z.string().uuid().safeParse(formData!.get("accountId")) + + if (!userId.success || !accountId.success) { + return { + status: "error", + messages: [ + { + title: "Invalid user or account ID", + body: "The user or account ID is not a valid UUID", + }, + ], + } + } + + // Check if trying to remove self + if (userId.data === formData!.get("sessionUserId")) { + return { + status: "error", + messages: [ + { + title: "User not removed", + body: "You cannot remove yourself", + }, + ], + } + } + + // Check if trying to remove owner + const result = await db + .delete(usersAccountsTable) + .where( + and( + eq(usersAccountsTable.userId, userId.data), + eq(usersAccountsTable.accountId, accountId.data), + not(eq(usersAccountsTable.role, "owner")), + ), + ) + + if (result.rowCount === 0) { + return { + status: "error", + messages: [ + { + title: "User not removed", + body: "The user you are trying to remove may be the owner or has already been removed", + }, + ], + } + } + + revalidatePath("/app/team") + + return { + status: "success", + } + }, + { + requireAuth: true, + requireAdmin: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/account/fetch-account-users-with-invites.ts b/templates/one-to-many/src/actions/account/fetch-account-users-with-invites.ts new file mode 100644 index 0000000..96a53c7 --- /dev/null +++ b/templates/one-to-many/src/actions/account/fetch-account-users-with-invites.ts @@ -0,0 +1,82 @@ +"use server" + +import { db } from "@/db" +import { usersTable } from "@/db/schema/users" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { eq, and, isNotNull } from "drizzle-orm" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { withQueryProtection } from "@/actions/action-middleware" + +/** + * Fetches both active users and pending invites for a given account. + * + * @param accountId - The unique identifier of the account + * @returns {Promise} An array combining both active users and pending invites, + * where each item is discriminated by the 'type' property ('user' | 'invite') + * + * @example + * const usersAndInvites = await fetchAccountUsersWithInvites('account123'); + * // Returns an array where each item is either: + * // - A user object with type: 'user', containing user details and their account role/status + * // - An invite object with type: 'invite', containing the invitation details + */ + +export const fetchAccountUsersWithInvites = withQueryProtection( + async (accountId: string) => { + const [userResults, inviteResults] = await db.batch([ + db + .select({ + id: usersTable.id, + name: usersTable.name, + email: usersTable.email, + image: usersTable.image, + role: usersAccountsTable.role, + }) + .from(usersAccountsTable) + .leftJoin(usersTable, eq(usersAccountsTable.userId, usersTable.id)) + .where( + and( + eq(usersAccountsTable.accountId, accountId), + isNotNull(usersTable.id), + ), + ), + + db + .select() + .from(inviteTokensTable) + .where(eq(inviteTokensTable.accountId, accountId)), + ]) + + return [ + ...userResults.map((user) => ({ type: "user" as const, ...user })), + ...inviteResults.map((invite) => ({ + type: "invite" as const, + email: invite.recipient, + id: invite.id, + status: "pending" as const, + role: "user" as const, + })), + ] + }, + { + requireAuth: true, + }, +) + +export type AccountUsersWithInvites = + | { + type: "user" + id: string + name: string | null + email: string | null + image: string | null + status: string + role: string + } + | { + type: "invite" + email: string + id: string + status: "pending" + role: string + } diff --git a/templates/one-to-many/src/actions/account/fetch-account-users.ts b/templates/one-to-many/src/actions/account/fetch-account-users.ts new file mode 100644 index 0000000..fa6898a --- /dev/null +++ b/templates/one-to-many/src/actions/account/fetch-account-users.ts @@ -0,0 +1,37 @@ +"use server" + +import { db } from "@/db" +import { usersTable } from "@/db/schema/users" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { UUID } from "@/lib/types" +import { and, eq, inArray } from "drizzle-orm" +import { withQueryProtection } from "@/actions/action-middleware" + +/** + * Fetches users associated with a specific account, optionally filtered by roles + * + * @param accountId - The unique identifier of the account + * @param roles - Optional array of roles to filter users by ('admin', 'user', 'owner') + * @returns Promise containing the joined users and their account relationships + */ + +export const fetchAccountUsers = withQueryProtection( + async (accountId: UUID, roles?: ("admin" | "user" | "owner")[]) => { + return db + .select() + .from(usersTable) + .innerJoin( + usersAccountsTable, + eq(usersAccountsTable.userId, usersTable.id), + ) + .where( + and( + eq(usersAccountsTable.accountId, accountId), + roles ? inArray(usersAccountsTable.role, roles) : undefined, + ), + ) + }, + { + requireAuth: true, + }, +) diff --git a/templates/one-to-many/src/actions/account/fetch-current-account.ts b/templates/one-to-many/src/actions/account/fetch-current-account.ts new file mode 100644 index 0000000..ba9ceff --- /dev/null +++ b/templates/one-to-many/src/actions/account/fetch-current-account.ts @@ -0,0 +1,57 @@ +"use server" + +import { accountsTable } from "@/db/schema/accounts" +import { db } from "@/db" +import { eq } from "drizzle-orm" +import { auth } from "@/lib/auth" +import { withQueryProtection } from "@/actions/action-middleware" + +/** + * Fetches the current user's account details from the database. + * This function requires an authenticated session to work properly. + * + * @throws {Error} When: + * - User is not signed in ("[Auth Error] Fetch current account requires a signed in user") + * - Account is not found in database ("[DB Error] No account found") + * + * @returns {Promise} The current user's account details + * @returns {Promise} When the user's session doesn't have an accountId + */ + +export const fetchCurrentAccount = withQueryProtection( + async () => { + const session = await auth() + + // If the user is not signed in, throw an error + if (!session || !session.user) { + throw new Error( + "[Auth Error] Fetch current account requires a signed in user", + ) + } + + const currentAccountId = session.user.accountId + + // If the user does not have an account, throw an error + if (!currentAccountId) { + return null + } + + // Get the account from the database + const account = await db + .select() + .from(accountsTable) + .where(eq(accountsTable.id, currentAccountId)) + .limit(1) + .then((rows) => rows[0]) + + // If the account was not found, throw an error + if (!account) { + throw new Error("[DB Error] No account found") + } + + return account + }, + { + requireAuth: true, + }, +) diff --git a/templates/one-to-many/src/actions/action-middleware.ts b/templates/one-to-many/src/actions/action-middleware.ts new file mode 100644 index 0000000..8112c9f --- /dev/null +++ b/templates/one-to-many/src/actions/action-middleware.ts @@ -0,0 +1,174 @@ +import { auth } from "@/lib/auth" +import { ServerActionResponse } from "@/lib/types" +import { fetchAccountUsers } from "@/actions/account/fetch-account-users" + +/** + * Type definition for server action functions that handle direct queries + * @template T - The return type of the action + * @template Args - Array type containing the function arguments + */ + +type ActionFunction = (...args: Args) => Promise + +/** + * Type definition for server action functions that handle form submissions + * @template Args - Array type containing additional function arguments + */ + +type ActionFunctionWithState = ( + prevState: ServerActionResponse | undefined, + formData: FormData, + ...args: Args +) => Promise + +/** + * Configuration options for the action middleware + * @interface + * @property {boolean} [requireAuth=true] - Whether the action requires user authentication + * @property {boolean} [requireAdmin] - Whether the action requires admin privileges + * @property {boolean} [validateFormData=true] - Whether to validate the FormData object + * @property {('form'|'query')} [type='form'] - The type of action being protected + */ + +interface MiddlewareOptions { + requireAuth?: boolean + requireAdmin?: boolean + validateFormData?: boolean + type?: "form" | "query" +} + +/** + * Validates user authentication and optionally admin status + * @param {boolean} requireAuth - Whether authentication is required + * @param {boolean} requireAdmin - Whether admin privileges are required + * @returns {Promise<{ isValid: boolean; error?: string }>} + */ + +async function validateAuth(requireAuth: boolean, requireAdmin?: boolean) { + const session = await auth() + + if (requireAuth && !session) { + return { + isValid: false, + error: "You must be logged in to perform this action", + } + } + + if (requireAdmin && session) { + const accountId = session.user.accountId + const users = await fetchAccountUsers(accountId, ["admin", "owner"]) + + if ( + Array.isArray(users) && + !users.some((user) => user.users.id === session?.user.id) + ) { + return { + isValid: false, + error: "You must be an admin or owner to perform this action", + } + } + } + + return { isValid: true } +} + +/** + * Higher-order function that wraps server actions with protection middleware + * @template T - The return type for query actions + * @template Args - Array type containing the function arguments + * @param {ActionFunction} action - The server action to protect + * @param {MiddlewareOptions} options - Configuration options for the middleware + * @returns {Promise} Protected server action function + * @throws {Error} When authentication or authorization fails + */ + +export function withQueryProtection( + action: ActionFunction, + options: Omit = { + requireAuth: true, + }, +) { + return async (...args: Args): Promise => { + const { isValid, error } = await validateAuth( + options.requireAuth ?? true, + options.requireAdmin, + ) + if (!isValid) { + throw new Error(error) + } + return await action(...args) + } +} + +/** + * Higher-order function that wraps form submission actions with protection middleware + * @template Args - Array type containing additional function arguments + * @param {ActionFunctionWithState} action - The form action to protect + * @param {MiddlewareOptions} options - Configuration options for the middleware + * @returns {Promise} Protected form action function + */ + +export function withFormProtection( + action: ActionFunctionWithState, + options: Omit = { + requireAuth: true, + validateFormData: true, + }, +) { + return async (...args: Args): Promise => { + try { + const { isValid, error } = await validateAuth( + options.requireAuth ?? true, + options.requireAdmin, + ) + if (!isValid) { + return { + status: "error", + messages: [{ title: "Unauthorized", body: error! }], + } + } + + const [prevState, formData, ...restArgs] = args + + if (prevState && typeof prevState !== "object") { + return { + status: "error", + messages: [ + { + title: "State Error", + body: "Previous state is not of the expected type", + }, + ], + } + } + + if (options.validateFormData && !(formData instanceof FormData)) { + return { + status: "error", + messages: [ + { + title: "Form Error", + body: "Form data is not a FormData object", + }, + ], + } + } + + return await action( + prevState as ServerActionResponse, + formData as FormData, + ...(restArgs as Args), + ) + } catch (error) { + return { + status: "error", + messages: [ + { + title: "Error", + body: error instanceof Error ? error.message : "An error occurred", + }, + ], + } + } + } +} diff --git a/templates/one-to-many/src/actions/auth/do-magic-auth.ts b/templates/one-to-many/src/actions/auth/do-magic-auth.ts new file mode 100644 index 0000000..268367e --- /dev/null +++ b/templates/one-to-many/src/actions/auth/do-magic-auth.ts @@ -0,0 +1,78 @@ +"use server" + +import { signIn } from "@/lib/auth" +import { ServerActionResponse } from "@/lib/types" +import { z } from "zod" + +/** + * Server action to sign in with a magic link. + * + * @param {ServerActionResponse | undefined} prevState - Previous state from the server action + * @param {FormData} [formData] - Form data containing the magic link information + * @throws {Error} When: + * - Form data is invalid + * - Environment variables are not set + * - No providers have been configured + * + * @returns {Promise} Object containing the status of the operation + */ + +export async function doMagicAuth( + prevState: ServerActionResponse | undefined, + formData?: FormData, +): Promise { + // If the environment variables are not set, throw an error + if (!process.env.RESEND_KEY || !process.env.RESEND_EMAIL_FROM) { + throw new Error( + "[Config Error] Magic link environment variables are not set", + ) + } + + // If the form didn't provide a FormData object, throw an error + if (!(formData instanceof FormData)) { + throw new Error("[Form Error] Form data is not a FormData object") + } + + // Check for a valid email address + const validatedEmail = z + .string() + .email("invalid_email") + .safeParse(formData.get("email")) + + if (!validatedEmail.success) { + return { + status: "error", + messages: [ + { + title: "That email address doesn't look right", + body: "Please try again with a valid email address.", + }, + ], + } + } + + try { + const email = validatedEmail.data + const callbackUrl = formData.get("callbackUrl") + + await signIn("resend", { + email, + redirect: false, + callbackUrl, + }) + + return { + status: "success", + } + } catch { + return { + status: "error", + messages: [ + { + title: "We've hit a problem", + body: "An unknown error has occurred.", + }, + ], + } + } +} diff --git a/templates/one-to-many/src/actions/auth/do-send-magic-link.ts b/templates/one-to-many/src/actions/auth/do-send-magic-link.ts new file mode 100644 index 0000000..a2c18ea --- /dev/null +++ b/templates/one-to-many/src/actions/auth/do-send-magic-link.ts @@ -0,0 +1,39 @@ +"use server" + +import { Resend } from "resend" + +export const doSendMagicLink = async (email: string, url: string) => { + const EmailClient = new Resend(process.env.RESEND_KEY) + + const { error } = await EmailClient.emails.send({ + from: `next-auth-template <${process.env.RESEND_EMAIL_FROM}>`, + to: email, + subject: "Sign in link for next-auth-template", + html: EmailMagicLink({ type: "html", context: { url } }), + text: EmailMagicLink({ type: "text", context: { url } }), + }) + + if (error) { + console.error(error) + } +} + +function EmailMagicLink({ + type, + context, +}: { + type: "html" | "text" + context: { url: string } +}) { + if (type === "html") { + return ` +

+ ` + } + + return ` + Click here to sign in: ${context.url} + ` +} diff --git a/templates/one-to-many/src/actions/auth/do-signout.ts b/templates/one-to-many/src/actions/auth/do-signout.ts new file mode 100644 index 0000000..0e9d4fe --- /dev/null +++ b/templates/one-to-many/src/actions/auth/do-signout.ts @@ -0,0 +1,15 @@ +"use server" + +import { signOut } from "@/lib/auth" + +/** + * Server action to sign out a user. + * + * @returns {Promise} + */ + +export async function doSignout() { + await signOut({ + redirectTo: "/", + }) +} diff --git a/templates/one-to-many/src/actions/auth/do-social-auth.ts b/templates/one-to-many/src/actions/auth/do-social-auth.ts new file mode 100644 index 0000000..db3dfab --- /dev/null +++ b/templates/one-to-many/src/actions/auth/do-social-auth.ts @@ -0,0 +1,51 @@ +"use server" + +import { authConfig, signIn } from "@/lib/auth" + +/** + * Server action to sign in with a social provider. + * + * @param {void | undefined} prevState - Previous state from the server action + * @param {FormData} [formData] - Form data containing the social provider information + * @throws {Error} When: + * - Form data is invalid + * - No provider was provided + * - Invalid provider was provided + * - No providers have been configured + * + * @returns {Promise} + */ + +export async function doSocialAuth( + prevState: void | undefined, + formData?: FormData, +): Promise { + // If the form didn't provide a FormData object, throw an error + if (!(formData instanceof FormData)) { + throw new Error("[Form Error] Form data is not a FormData object") + } + + const provider = formData.get("provider") + + // If the form didn't give us a provider, throw an error + if (!provider) { + throw new Error("[Form Error] No provider was provided") + } + + const configuredProviders = authConfig.providers + + // If no providers are configured, throw an error + if (!configuredProviders) { + throw new Error("[Config Error] No providers have been configured") + } + + // If the provided provider is not in the configured providers, throw an error + if (!configuredProviders.some((p) => p.id === provider)) { + throw new Error("[Form Error] Invalid provider was provided") + } + + const callbackUrl = formData.get("callbackUrl") + + // Sign in with the provider + await signIn(String(provider), { callbackUrl }) +} diff --git a/templates/one-to-many/src/actions/invite/do-invite-accept.ts b/templates/one-to-many/src/actions/invite/do-invite-accept.ts new file mode 100644 index 0000000..57f0320 --- /dev/null +++ b/templates/one-to-many/src/actions/invite/do-invite-accept.ts @@ -0,0 +1,142 @@ +"use server" + +import { ServerActionResponse } from "@/lib/types" +import { withFormProtection } from "@/actions/action-middleware" +import { fetchInvite } from "@/actions/invite/fetch-invite" +import { auth } from "@/lib/auth" +import { db } from "@/db" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { eq } from "drizzle-orm" + +/** + * Accepts an invite by validating the invite token, ensuring the invite is not expired, + * and confirming the invite is intended for the current user. If valid, the user is added + * to the account and the invite is deleted. + * + * @param {ServerActionResponse | undefined} prevState - The previous state of the server action. + * @param {FormData} [formData] - The form data containing the invite token. + * @returns {Promise} A promise that resolves to the server action response. + * + * @throws Will return an error response if: + * - The invite token is not provided. + * - The invite does not exist. + * - The invite has expired. + * - The invite is not intended for the current user. + * - There is an error adding the user to the account. + * - There is an error deleting the invite. + */ + +export const doInviteAccept = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const inviteToken = formData!.get("inviteToken") as string + + // Check invite token is provided + if (!inviteToken) { + return { + status: "error", + messages: [ + { + title: "Invite token is required", + body: "Please provide an invite token.", + }, + ], + } + } + + // Get the invite + const invite = await fetchInvite(inviteToken) + + if (!invite) { + return { + status: "error", + messages: [ + { + title: "Invite not found", + body: "The invite you are trying to accept does not exist.", + }, + ], + } + } + + // Check invite is not expired + if (invite.expiresAt < new Date()) { + return { + status: "error", + messages: [ + { + title: "Invite expired", + body: "The invite you are trying to accept has expired.", + }, + ], + } + } + + const userSession = await auth() + + // Check the current user's email address matches the invite's email address + if (invite.recipient !== userSession?.user.email) { + return { + status: "error", + messages: [ + { + title: "Invite not for you", + body: `The invite you are trying to accept is not for you.`, + }, + ], + } + } + + // Perform operations in a batch + const batchResults = await db.batch([ + // Add this user to the account via users_accounts + db.insert(usersAccountsTable).values({ + userId: userSession?.user.id, + accountId: invite.account.id, + }), + + // Delete the invite + db + .delete(inviteTokensTable) + .where(eq(inviteTokensTable.token, inviteToken)), + ]) + + // Verify the batch operations + const [insertResult, deleteResult] = batchResults + + if (!insertResult) { + return { + status: "error", + messages: [ + { + title: "Failed to add user to the account.", + body: "An error occurred while adding the user to the account.", + }, + ], + } + } + + if (!deleteResult) { + return { + status: "error", + messages: [ + { + title: "Failed to delete the invite.", + body: "An error occurred while deleting the invite.", + }, + ], + } + } + + return { + status: "success", + } + }, + { + requireAuth: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/invite/do-invite-create.ts b/templates/one-to-many/src/actions/invite/do-invite-create.ts new file mode 100644 index 0000000..3779c3d --- /dev/null +++ b/templates/one-to-many/src/actions/invite/do-invite-create.ts @@ -0,0 +1,258 @@ +"use server" + +import { db } from "@/db" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { auth } from "@/lib/auth" +import { ServerActionResponse, UUID } from "@/lib/types" +import { type InviteToken } from "@/db/schema/invite_tokens" +import { Resend } from "resend" +import { z } from "zod" +import { usersTable } from "@/db/schema/users" +import { eq } from "drizzle-orm" +import { revalidatePath } from "next/cache" +import { withFormProtection } from "@/actions/action-middleware" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { EmailInvite } from "@/components/layout/email-invite" +import { accountsTable } from "@/db/schema/accounts" + +/** + * Creates and sends an invitation to join a team/account + * + * @param prevState - Previous server action response state (unused but required for Next.js Server Actions) + * @param formData - Form data containing accountId and email + * @returns {Promise} Response object containing status, data, and optional messages + * + * Protected by withFormProtection middleware which ensures: + * - User is authenticated + * - User has admin permissions + * - FormData is present + * + * The function performs the following checks: + * 1. Validates the email address format + * 2. Verifies user session exists + * 3. Checks if the email is already registered + * 4. Creates an invite token with 24-hour expiry + * 5. Sends an invitation email via Resend + * 6. Revalidates the team page cache + */ + +export const doInviteCreate = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const accountId = formData!.get("accountId") as UUID + const unvalidatedEmail = formData!.get("email") + const validatedEmail = z.string().email().safeParse(unvalidatedEmail) + + // Check the email address is valid using zod + if (!validatedEmail.success) { + return { + status: "error", + data: { email: unvalidatedEmail }, + messages: [ + { + title: "Email address is not valid", + body: "Please check for typos and try again.", + }, + ], + } + } + + const session = await auth() + + if (!session) { + return { + status: "error", + data: { email: validatedEmail.data }, + messages: [ + { + title: "Unauthorized", + body: "You must be logged in to invite a user", + }, + ], + } + } + + // Check if the user already has an account in users_accounts join with the user's table + const existingUserAccount = await db + .select() + .from(usersAccountsTable) + .innerJoin(usersTable, eq(usersAccountsTable.userId, usersTable.id)) + .where(eq(usersTable.email, validatedEmail.data)) + .limit(1) + + if (existingUserAccount.length > 0) { + if (existingUserAccount[0].users_accounts.accountId === accountId) { + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Already a team member", + body: "A user with this email address already belongs to this account.", + }, + ], + } + } + + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Already part of another team", + body: "A user with this email address already belongs to another account.", + }, + ], + } + } + + // Get the account name, to use in the email and check it exists + const account = await db + .select({ name: accountsTable.name }) + .from(accountsTable) + .where(eq(accountsTable.id, accountId)) + .limit(1) + + if (!account) { + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Account does not exist", + body: "The account you are trying to invite to does not exist.", + }, + ], + } + } + + let invite: InviteToken[] | undefined + + try { + invite = await db + .insert(inviteTokensTable) + .values({ + accountId, + inviterId: session.user.id, + recipient: validatedEmail.data, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours + }) + .returning() + + if (!invite) { + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "An expected error occurred", + body: "An error occurred when attempting to create the invite.", + }, + ], + } + } + } catch (error) { + const constraint = (error as { constraint?: string }).constraint + + if (constraint === "invite_tokens_identifier_unique") { + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Invite already sent", + body: "A valid invite has already been sent to this email address.", + }, + ], + } + } + + console.error(error) + + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "An unexpected error occurred", + body: "An error occurred when attempting to create the invite.", + }, + ], + } + } + + if (!invite) { + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Invite was not created", + body: "An error occurred while creating the invite.", + }, + ], + } + } + + const EmailClient = new Resend(process.env.RESEND_KEY) + + const { error } = await EmailClient.emails.send({ + from: `next-auth-template <${process.env.RESEND_EMAIL_FROM}>`, + to: validatedEmail.data, + subject: `${session.user.name} invited you to join ${account[0].name}`, + react: EmailInvite({ + context: { + token: invite[0].token, + account: { + name: account[0].name, + }, + }, + }) as React.ReactElement, + }) + + if (error) { + console.error(error) + + return { + status: "error", + data: { + email: validatedEmail.data, + }, + messages: [ + { + title: "Invite was not created", + body: "An error occurred while sending the invite.", + }, + ], + } + } + + revalidatePath("/app/team") + + return { + status: "success", + data: invite, + } + }, + { + requireAuth: true, + requireAdmin: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/invite/do-remove-invite.ts b/templates/one-to-many/src/actions/invite/do-remove-invite.ts new file mode 100644 index 0000000..b22073d --- /dev/null +++ b/templates/one-to-many/src/actions/invite/do-remove-invite.ts @@ -0,0 +1,75 @@ +"use server" + +import { db } from "@/db" +import { eq } from "drizzle-orm" +import { revalidatePath } from "next/cache" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { z } from "zod" +import { withFormProtection } from "@/actions/action-middleware" +import { ServerActionResponse } from "@/lib/types" + +/** + * Removes an invitation token + * + * @param prevState - Previous server action response state (unused but required for Next.js Server Actions) + * @param formData - Form data containing inviteId and accountId + * @returns {Promise} Response object containing status and optional messages + * + * Protected by withFormProtection middleware which ensures: + * - User is authenticated + * - User has admin permissions + * - FormData is present + * + * The function performs the following: + * 1. Validates the invite ID is a valid UUID + * 2. Attempts to delete the invite token + * 3. Revalidates the team page cache + */ + +export const doRemoveInvite = withFormProtection( + async ( + prevState: ServerActionResponse | undefined, + formData?: FormData, + ): Promise => { + const inviteId = z.string().uuid().safeParse(formData!.get("inviteId")) + + if (!inviteId.success) { + return { + status: "error", + messages: [ + { + title: "Invalid invite ID", + body: "The invite ID is not a valid UUID", + }, + ], + } + } + + const result = await db + .delete(inviteTokensTable) + .where(eq(inviteTokensTable.id, inviteId.data)) + + if (result.rowCount === 0) { + return { + status: "error", + messages: [ + { + title: "Invite not deleted", + body: "The invite was not found", + }, + ], + } + } + + revalidatePath("/app/team") + + return { + status: "success", + } + }, + { + requireAuth: true, + requireAdmin: true, + validateFormData: true, + }, +) diff --git a/templates/one-to-many/src/actions/invite/fetch-invite-full.ts b/templates/one-to-many/src/actions/invite/fetch-invite-full.ts new file mode 100644 index 0000000..98c2b59 --- /dev/null +++ b/templates/one-to-many/src/actions/invite/fetch-invite-full.ts @@ -0,0 +1,82 @@ +import { db } from "@/db" +import { eq } from "drizzle-orm" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { withQueryProtection } from "../action-middleware" +import { usersTable } from "@/db/schema/users" +import { accountsTable } from "@/db/schema/accounts" + +/** + * Fetches the full details of an invite using the provided invite token. + * + * This function retrieves information about the invite, including the associated + * account and inviter details, by joining the invite tokens, accounts, and users tables. + * + * @param {string} inviteToken - The token of the invite to fetch details for. + * @returns {Promise<{ + * id: string, + * token: string, + * expiresAt: Date, + * account: { + * id?: string, + * name?: string | null + * }, + * inviter: { + * id?: string, + * name?: string | null, + * email?: string | null, + * image?: string | null + * } + * }>} A promise that resolves to an object containing the invite details. + * + * @throws {Error} Throws an error if authentication or authorization fails. + * @throws {Error} Throws an error if the database query fails. + */ + +export const fetchInviteFull = withQueryProtection( + async (inviteToken: string) => { + const rawResults = await db + .select({ + id: inviteTokensTable.id, + token: inviteTokensTable.token, + expiresAt: inviteTokensTable.expiresAt, + accountId: accountsTable.id, + accountName: accountsTable.name, + inviterId: usersTable.id, + inviterName: usersTable.name, + inviterEmail: usersTable.email, + inviterImage: usersTable.image, + }) + .from(inviteTokensTable) + .innerJoin( + accountsTable, + eq(inviteTokensTable.accountId, accountsTable.id), + ) + .innerJoin(usersTable, eq(inviteTokensTable.inviterId, usersTable.id)) + .where(eq(inviteTokensTable.token, inviteToken)) + .limit(1) + .then((rows) => rows[0]) + + const inviteFull = rawResults + ? { + id: rawResults.id, + token: rawResults.token, + expiresAt: rawResults.expiresAt, + account: { + id: rawResults.accountId, + name: rawResults.accountName, + }, + inviter: { + id: rawResults.inviterId, + name: rawResults.inviterName, + email: rawResults.inviterEmail, + image: rawResults.inviterImage, + }, + } + : null + + return inviteFull + }, + { + requireAuth: false, + }, +) diff --git a/templates/one-to-many/src/actions/invite/fetch-invite.ts b/templates/one-to-many/src/actions/invite/fetch-invite.ts new file mode 100644 index 0000000..6e61fa4 --- /dev/null +++ b/templates/one-to-many/src/actions/invite/fetch-invite.ts @@ -0,0 +1,80 @@ +import { db } from "@/db" +import { eq } from "drizzle-orm" +import { inviteTokensTable } from "@/db/schema/invite_tokens" +import { withQueryProtection } from "../action-middleware" +import { accountsTable } from "@/db/schema/accounts" + +/** + * Fetches invite details based on the provided invite token or recipient. + * + * This function queries the database to retrieve invite information, including + * the associated account details, using either an invite token or a recipient identifier. + * + * @param {string} [inviteToken] - The token of the invite to fetch details for. + * @param {string} [recipient] - The recipient identifier to fetch invite details for. + * @returns {Promise<{ + * token: string, + * recipient: string, + * expiresAt: Date, + * account: { + * id?: string, + * name?: string | null + * }, + * inviterId: string + * }>} A promise that resolves to an object containing the invite details. + * + * @throws {Error} Throws an error if neither inviteToken nor recipient is provided. + * @throws {Error} Throws an error if authentication or authorization fails. + * @throws {Error} Throws an error if the database query fails. + */ + +export const fetchInvite = withQueryProtection( + async (inviteToken?: string, recipient?: string) => { + if (!inviteToken && !recipient) { + throw new Error("Either inviteToken or recipient must be provided.") + } + + const query = db + .select({ + token: inviteTokensTable.token, + recipient: inviteTokensTable.recipient, + accountId: accountsTable.id, + accountName: accountsTable.name, + expiresAt: inviteTokensTable.expiresAt, + inviterId: inviteTokensTable.inviterId, + }) + .from(inviteTokensTable) + .innerJoin( + accountsTable, + eq(inviteTokensTable.accountId, accountsTable.id), + ) + + if (inviteToken) { + query.where(eq(inviteTokensTable.token, inviteToken)) + } else if (recipient) { + query.where(eq(inviteTokensTable.recipient, recipient)) + } + + const rawResults = await query.limit(1).then((rows) => rows[0]) + + const invite = rawResults + ? { + token: rawResults.token, + recipient: rawResults.recipient, + expiresAt: rawResults.expiresAt, + account: rawResults.accountId + ? { + id: rawResults.accountId, + name: rawResults.accountName, + } + : {}, + inviterId: rawResults.inviterId, + } + : null + + return invite + }, + { + requireAuth: false, + }, +) diff --git a/templates/one-to-many/src/actions/user/fetch-current-user.ts b/templates/one-to-many/src/actions/user/fetch-current-user.ts new file mode 100644 index 0000000..f7728b3 --- /dev/null +++ b/templates/one-to-many/src/actions/user/fetch-current-user.ts @@ -0,0 +1,77 @@ +"use server" + +import { auth } from "@/lib/auth" +import { db } from "@/db" +import { usersTable } from "@/db/schema/users" +import { eq } from "drizzle-orm" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { accountsTable } from "@/db/schema/accounts" +import { withQueryProtection } from "../action-middleware" + +/** + * Fetches the current authenticated user along with their associated account details. + * + * @returns {Promise<{ + * id: string; + * name: string | null; + * email: string | null; + * image: string | null; + * account: { + * id?: string; + * name?: string | null; + * role?: string | null; + * status?: string | null; + * }; + * } | null>} Returns user data with account details if authenticated, null otherwise + */ + +export const fetchCurrentUser = withQueryProtection( + async () => { + const session = await auth() + if (!session) return null + + const rawResults = await db + .select({ + id: usersTable.id, + name: usersTable.name, + email: usersTable.email, + image: usersTable.image, + accountId: accountsTable.id, + accountName: accountsTable.name, + accountRole: usersAccountsTable.role, + }) + .from(usersTable) + .leftJoin( + usersAccountsTable, + eq(usersTable.id, usersAccountsTable.userId), + ) + .leftJoin( + accountsTable, + eq(usersAccountsTable.accountId, accountsTable.id), + ) + .where(eq(usersTable.id, session.user.id)) + .limit(1) + .then((rows) => rows[0]) + + const userWithAccount = rawResults + ? { + id: rawResults.id, + name: rawResults.name, + email: rawResults.email, + image: rawResults.image, + account: rawResults.accountId + ? { + id: rawResults.accountId, + name: rawResults.accountName, + role: rawResults.accountRole, + } + : {}, + } + : null + + return userWithAccount + }, + { + requireAuth: true, + }, +) diff --git a/templates/one-to-many/src/actions/user/fetch-user-accounts.ts b/templates/one-to-many/src/actions/user/fetch-user-accounts.ts new file mode 100644 index 0000000..f0bb983 --- /dev/null +++ b/templates/one-to-many/src/actions/user/fetch-user-accounts.ts @@ -0,0 +1,26 @@ +"use server" + +import { db } from "@/db" +import { usersAccountsTable } from "@/db/schema/users_accounts" +import { UUID } from "@/lib/types" +import { eq } from "drizzle-orm" +import { withQueryProtection } from "@/actions/action-middleware" + +/** + * Fetches all accounts associated with a specific user. + * + * @param userId - The unique identifier of the user + * @returns {Promise} A promise that resolves to an array of UserAccount objects + */ + +export const fetchUserAccounts = withQueryProtection( + async (userId: UUID) => { + return db + .select() + .from(usersAccountsTable) + .where(eq(usersAccountsTable.userId, userId)) + }, + { + requireAuth: true, + }, +) diff --git a/templates/one-to-many/src/app/(app)/app/layout.tsx b/templates/one-to-many/src/app/(app)/app/layout.tsx new file mode 100644 index 0000000..68fd189 --- /dev/null +++ b/templates/one-to-many/src/app/(app)/app/layout.tsx @@ -0,0 +1,66 @@ +import "../../globals.css" + +import { fetchCurrentUser } from "@/actions/user/fetch-current-user" +import { AppSidebar } from "@/components/layout/app-sidebar" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" +import { redirect } from "next/navigation" +import { cookies } from "next/headers" + +export default async function AppLayout({ + children, +}: { + children: React.ReactNode +}) { + const currentUser = await fetchCurrentUser() + + if (!currentUser || !currentUser.id) { + return redirect("/signin") + } + + // Persist the open/closed state of the sidebar in a cookie + const cookieStore = await cookies() + const sidebarDefaultOpen = cookieStore.get("sidebar:state")?.value === "true" + + return ( + + + + + +
+
+ + + + + + Inbox + + + +
+
+
+ {children} +
+
+
+ + + ) +} diff --git a/templates/one-to-many/src/app/(app)/app/page.tsx b/templates/one-to-many/src/app/(app)/app/page.tsx new file mode 100644 index 0000000..a21cfbd --- /dev/null +++ b/templates/one-to-many/src/app/(app)/app/page.tsx @@ -0,0 +1,55 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { BookHeart, Github } from "lucide-react" +import Link from "next/link" + +export default function Page() { + return ( +
+ + + 🎉 You're in! + + +

+ Of course, none of this is a real app. It's just a demo to show + the registration and authentication flows. But it's ready for + you to get building! +

+

+ While you're here, here's some things to + try: +

+
    +
  • + + Invite a new user to your account + + , manage your team and roles. +
  • +
  • + You can sign out using the user box at the bottom of the sidebar. +
  • +
  • + After logging out, try signing in again with a different method. +
  • +
+
+ + +
+
+
+
+ ) +} diff --git a/templates/one-to-many/src/app/(app)/app/team/page.tsx b/templates/one-to-many/src/app/(app)/app/team/page.tsx new file mode 100644 index 0000000..ce76d20 --- /dev/null +++ b/templates/one-to-many/src/app/(app)/app/team/page.tsx @@ -0,0 +1,127 @@ +import InviteUserForm from "@/components/invite-user-form" +import { auth } from "@/lib/auth" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + AccountUsersWithInvites, + fetchAccountUsersWithInvites, +} from "@/actions/account/fetch-account-users-with-invites" +import { CircleUser } from "lucide-react" +import RemoveUser from "@/components/remove-user" +import RemoveInvite from "@/components/remove-invite" +import { UserRoleSelect } from "@/components/user-role-select" + +export default async function TeamPage() { + const session = await auth() + const accountId = session?.user?.accountId + + if (!accountId) { + throw new Error("No account found") + } + + const usersWithRoles = await fetchAccountUsersWithInvites(accountId) + const currentUserRole = usersWithRoles.find( + (user) => user.id === session?.user?.id, + )?.role + + if (!currentUserRole) { + throw new Error("No current user role found") + } + + return ( +
+

Team

+

+ Manage your team members and their roles. +

+
+
+ + + + + Name + Email + + {(currentUserRole === "owner" || + currentUserRole === "admin") && ( + + )} + + + + + {usersWithRoles.map((user) => { + if (user.type === "user") { + return ( + + + + + + + + + + + {user.name || "-"} + + {user.email} + + + + + + ) + } + + if (user.type === "invite") { + return ( + + + + + + + + + - + {user.email} + +

+ Invite pending... +

+ +
+
+ ) + } + })} +
+
+
+
+
+ ) +} diff --git a/templates/one-to-many/src/app/(auth)/invite/[...token]/page.tsx b/templates/one-to-many/src/app/(auth)/invite/[...token]/page.tsx new file mode 100644 index 0000000..5e4a259 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/invite/[...token]/page.tsx @@ -0,0 +1,56 @@ +import { fetchInvite } from "@/actions/invite/fetch-invite" +import { fetchInviteFull } from "@/actions/invite/fetch-invite-full" +import { ErrorCard } from "@/components/error-card" +import { InviteCard } from "@/components/invite-card" +import { auth } from "@/lib/auth" +import { notFound } from "next/navigation" + +export default async function InvitePage({ + params, +}: { + params: Promise<{ token: string }> +}) { + const session = await auth() + const inviteToken = (await params).token[0] + + if (session) { + const invite = await fetchInviteFull(inviteToken) + + // If invite is not found + if (!invite) { + return notFound() + } + + // If invite is past expiry date + if (invite.expiresAt < new Date()) { + return This invite has expired. + } + + // If user is already part of an account + if (session.user.accountId !== undefined) { + return ( + + You cannot be part of more than one account. + + ) + } + + // If user is not part of an account + return + } + + const invite = await fetchInvite(inviteToken) + + // If invite is not found + if (!invite) { + return notFound() + } + + // If invite is past expiry date + if (invite.expiresAt < new Date()) { + return This invite has expired. + } + + // If user is not signed in + return +} diff --git a/templates/one-to-many/src/app/(auth)/layout.tsx b/templates/one-to-many/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..2b23738 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next" +import "../globals.css" + +export const metadata: Metadata = { + title: "Next.js + Auth.js Template", + description: + "Sign up and auth, super quick, with database-backed sessions, social sign in, and magic links.", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + +
{children}
+ + + ) +} diff --git a/templates/one-to-many/src/app/(auth)/signin/page.tsx b/templates/one-to-many/src/app/(auth)/signin/page.tsx new file mode 100644 index 0000000..d79d191 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/signin/page.tsx @@ -0,0 +1,26 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import Link from "next/link" +import { AccountSignInForm } from "@/components/account-signin-form" + +export default function SignInPage() { + return ( + + + Sign in + Select an option below to sign in. + + + +
+ Don't have an account? +
+
+
+ ) +} diff --git a/templates/one-to-many/src/app/(auth)/signout/page.tsx b/templates/one-to-many/src/app/(auth)/signout/page.tsx new file mode 100644 index 0000000..99c2a29 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/signout/page.tsx @@ -0,0 +1,17 @@ +import { DoSignout } from "@/components/signout" +import { Loader2 } from "lucide-react" + +export default function SignoutPage() { + return ( + <> +
+ 👋 +

+ + Signing you out... +

+
+ + + ) +} diff --git a/templates/one-to-many/src/app/(auth)/signup/page.tsx b/templates/one-to-many/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..d580b66 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/signup/page.tsx @@ -0,0 +1,36 @@ +import GoogleLogo from "@/components/svg/google-logo" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { MagicSignInButton } from "@/components/magic-sign-in-button" +import { SocialSignInButton } from "@/components/social-sign-in-button" +import Link from "next/link" + +export default function SignInPage() { + return ( + + + Create an account + + Select an option below to quickly get started. + + + +
+ + + Sign up with Google + +
+ {process.env.RESEND_KEY && } +
+ Already have an account? +
+
+
+ ) +} diff --git a/templates/one-to-many/src/app/(auth)/verify/page.tsx b/templates/one-to-many/src/app/(auth)/verify/page.tsx new file mode 100644 index 0000000..a6d2872 --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/verify/page.tsx @@ -0,0 +1,16 @@ +import { Card, CardContent } from "@/components/ui/card" + +export default function VerifyPage() { + return ( + + +
+ 🎉 +

+ Check your inbox (and maybe your spam folder) for a link. +

+
+
+
+ ) +} diff --git a/templates/one-to-many/src/app/(auth)/welcome/page.tsx b/templates/one-to-many/src/app/(auth)/welcome/page.tsx new file mode 100644 index 0000000..e72be3d --- /dev/null +++ b/templates/one-to-many/src/app/(auth)/welcome/page.tsx @@ -0,0 +1,12 @@ +import { AccountSetupForm } from "@/components/account-setup-form" +import { fetchCurrentUser } from "@/actions/user/fetch-current-user" + +export default async function SignUpWelcomePage() { + const currentUser = await fetchCurrentUser() + + if (!currentUser) { + throw new Error("currentUser has not been passed to SignUpWelcomePage") + } + + return +} diff --git a/templates/one-to-many/src/app/(site)/layout.tsx b/templates/one-to-many/src/app/(site)/layout.tsx new file mode 100644 index 0000000..75499ee --- /dev/null +++ b/templates/one-to-many/src/app/(site)/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next" +import "../globals.css" + +export const metadata: Metadata = { + title: "Next.js + Auth.js Template", + description: + "Sign up and auth, super quick, with database-backed sessions, social sign in, and magic links.", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} diff --git a/templates/one-to-many/src/app/(site)/page.tsx b/templates/one-to-many/src/app/(site)/page.tsx new file mode 100644 index 0000000..23073e4 --- /dev/null +++ b/templates/one-to-many/src/app/(site)/page.tsx @@ -0,0 +1,276 @@ +/* eslint-disable @next/next/no-img-element */ + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + CircleUser, + DatabaseZapIcon, + KeyRound, + LayoutTemplate, + LockKeyhole, + WandSparkles, +} from "lucide-react" +import Link from "@/components/ui/link" +import Image from "next/image" +import { Fragment } from "react" + +export default function AppPage() { + const features = [ + { + name: "Custom sign in and sign up pages", + description: "Simple, interactive-rich, and server action-backed", + icon: LayoutTemplate, + }, + { + name: "Database-backed sessions", + description: "Not using Postgresql? Drizzle lets you use any db", + icon: DatabaseZapIcon, + }, + { + name: "Google Sign-in ready for config", + description: "Easily add more via nextauth.js", + icon: KeyRound, + }, + { + name: "Magic Links via Resend ready for config", + description: "Or use any other email providers", + icon: WandSparkles, + }, + { + name: "Basic account creation and set up", + description: "A pre-built account setup page ready to build on", + icon: CircleUser, + }, + { + name: "Protected paths via middleware", + description: "Easily protect app routes with auth middleware", + icon: LockKeyhole, + }, + ] + + const packageDepedenciesFilename = "templates%2Fone-to-one%2Fpackage.json" + + const packageDependencies = [ + { + name: "next", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/next?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "next-auth", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/next-auth?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "react", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/react?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "drizzle-orm", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/drizzle-orm?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "zod", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/zod?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "@neondatabase/serverless", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/@neondatabase%2Fserverless?filename=${packageDepedenciesFilename}`, + isCore: true, + }, + { + name: "shadcn/ui", + shieldsUrl: "https://img.shields.io/badge/shadcn%2Fui-gray", + }, + { + name: "tailwindcss", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/dev/tailwindcss?filename=${packageDepedenciesFilename}`, + }, + { + name: "@radix-ui", + shieldsUrl: "https://img.shields.io/badge/radix--ui-gray", + }, + { + name: "lucide-react", + shieldsUrl: + `https://img.shields.io/github/package-json/dependency-version/jakeisonline/next-auth-template/lucide-react?filename=${packageDepedenciesFilename}`, + }, + ] + + return ( + <> +
+ A decorative background image +
+
+
+
+ next-auth-template +
+
+ +
+
+
+
+
+

+ Sign up and auth, super quick +

+

+ With database-backed sessions, social sign in, and magic links. +

+
+ + +
+
+ + Clone on GitHub + {" "} + or{" "} + + Deploy on Vercel + +
+
+
+ + + Features + + +
+ {features.map((feature) => ( + +
+ + {feature.name} +
+
+ {feature.description} +
+
+ ))} +
+

+ + See all the features and how to use them in the documentation + +

+
+
+ + + + Core Dependencies + + Required to make the basics work. They could be replaced, but it + would defeat the point of this template. + + + +
    + {packageDependencies.map((dependency) => { + if (!dependency.isCore) return null + + return ( +
  • + {dependency.name} +
  • + ) + })} +
+

+ Other Dependencies +

+

+ Adds a bit of style or utility, could easily be replaced. +

+
    + {packageDependencies.map((dependency) => { + if (dependency.isCore) return null + + return ( +
  • + {dependency.name} +
  • + ) + })} +
+

+ + See the full list of dependencies in package.json + +

+
+
+
+
+
+
+ 👋 a template by Jake +
+
+ + ) +} diff --git a/templates/one-to-many/src/app/api/auth/[...nextauth]/route.ts b/templates/one-to-many/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..5951f83 --- /dev/null +++ b/templates/one-to-many/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/lib/auth" +export const { GET, POST } = handlers diff --git a/templates/one-to-many/src/app/favicon.ico b/templates/one-to-many/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/templates/one-to-many/src/app/globals.css b/templates/one-to-many/src/app/globals.css new file mode 100644 index 0000000..55db8b0 --- /dev/null +++ b/templates/one-to-many/src/app/globals.css @@ -0,0 +1,96 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --link: 221 83% 53%; + --link-hover: 225.93 70.73% 40.2%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/templates/one-to-many/src/components/accept-invite-button.tsx b/templates/one-to-many/src/components/accept-invite-button.tsx new file mode 100644 index 0000000..be0c6ef --- /dev/null +++ b/templates/one-to-many/src/components/accept-invite-button.tsx @@ -0,0 +1,52 @@ +"use client" + +import { doInviteAccept } from "@/actions/invite/do-invite-accept" +import { Button } from "@/components/ui/button" +import { useActionState, useEffect } from "react" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { AlertCircle, Loader2 } from "lucide-react" +import { useRouter } from "next/navigation" +import { useFormGroupIsSubmitting } from "@/hooks/use-form-group-is-submitting" + +export function AcceptInviteButton({ inviteToken }: { inviteToken: string }) { + const router = useRouter() + const [state, formAction, isPending] = useActionState( + doInviteAccept, + undefined, + ) + + const { formGroupIsSubmitting, setFormGroupIsSubmitting } = + useFormGroupIsSubmitting() + + useEffect(() => { + if (state?.status === "success") { + router.push("/welcome") + } + + if (isPending) { + setFormGroupIsSubmitting(isPending) + } + }, [state, isPending]) + + // We want to keep the form disabled if the action is successful, because we're going to redirect the user and the form is not reusable. + const isDisabled = formGroupIsSubmitting || state?.status === "success" + + return ( +
+ + {state?.status === "error" && ( + + + + {state?.messages?.[0]?.title} + + {state?.messages?.[0]?.body} + + )} + +
+ ) +} diff --git a/templates/one-to-many/src/components/account-setup-form.tsx b/templates/one-to-many/src/components/account-setup-form.tsx new file mode 100644 index 0000000..5acc9f3 --- /dev/null +++ b/templates/one-to-many/src/components/account-setup-form.tsx @@ -0,0 +1,99 @@ +"use client" + +import { doAccountSetup } from "@/actions/account/do-account-setup" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useActionState, useEffect, useState } from "react" +import { Button } from "@/components/ui/button" +import { useRouter } from "next/navigation" +import { Loader2 } from "lucide-react" +import { serverActionResponseSchema } from "@/lib/schemas" +import { type CurrentUser } from "@/lib/types" + +export function AccountSetupForm({ + currentUser, +}: { + currentUser: CurrentUser +}) { + if (!currentUser) { + throw new Error("currentUser has not been passed to AccountSetupForm") + } + + const [state, formAction, isPending] = useActionState( + doAccountSetup, + undefined, + ) + const [userName, setUserName] = useState(currentUser.name ?? "") + const router = useRouter() + + // Validate the state response is what we're expecting + const validState = serverActionResponseSchema.safeParse(state) + + // Check if the state is valid + if (state !== undefined && !validState.success) { + throw new Error("Invalid state response from the server") + } + + // We want to keep the form disabled if the action is successful, because we're going to redirect the user and the form is not reusable. + const isDisabled = isPending || state?.status === "success" + + useEffect(() => { + if (state?.status === "success") { + router.push("/app") + return + } + }, [state]) + + return ( + + + Before you get started + + Let's make sure we have the right information. + + +
+ +
+ + setUserName(e.target.value)} + disabled={isDisabled} + required + autoFocus + /> +

+ This is how you would like to be addressed. +

+
+
+ + +

+ This name will be visible on the interface, and to other users in + this account. +

+
+
+ + + +
+
+ ) +} diff --git a/templates/one-to-many/src/components/account-signin-form.tsx b/templates/one-to-many/src/components/account-signin-form.tsx new file mode 100644 index 0000000..4d75555 --- /dev/null +++ b/templates/one-to-many/src/components/account-signin-form.tsx @@ -0,0 +1,32 @@ +"use client" + +import { FormGroupContextProvider } from "@/hooks/use-form-group-is-submitting" +import { SocialSignInButton } from "@/components/social-sign-in-button" +import { MagicSignInButton } from "@/components/magic-sign-in-button" +import GoogleLogo from "@/components/svg/google-logo" +import { cn } from "@/lib/utils" + +type AccountSignInFormProps = { + showMagicSignIn?: boolean + callbackUrl?: string + className?: string +} + +export function AccountSignInForm({ + showMagicSignIn = false, + callbackUrl, + className, + ...props +}: AccountSignInFormProps) { + return ( + +
+ + + Sign in with Google + +
+ {showMagicSignIn && } +
+ ) +} diff --git a/templates/one-to-many/src/components/error-card.tsx b/templates/one-to-many/src/components/error-card.tsx new file mode 100644 index 0000000..848145c --- /dev/null +++ b/templates/one-to-many/src/components/error-card.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Avatar } from "@radix-ui/react-avatar" +import { AvatarFallback } from "@/components/ui/avatar" +import { Ban, CircleX } from "lucide-react" + +export function ErrorCard({ + icon = "default", + children, +}: { + icon?: "default" | "ban" + children: React.ReactNode +}) { + return ( + + + + + {icon === "ban" ? ( + + ) : ( + + )} + + + {children} + + + + ) +} diff --git a/templates/one-to-many/src/components/invite-accept-form.tsx b/templates/one-to-many/src/components/invite-accept-form.tsx new file mode 100644 index 0000000..19cb596 --- /dev/null +++ b/templates/one-to-many/src/components/invite-accept-form.tsx @@ -0,0 +1,35 @@ +"use client" + +import { AcceptInviteButton } from "./accept-invite-button" +import { Button } from "./ui/button" +import Link from "next/link" +import { Alert, AlertDescription } from "./ui/alert" +import { Sparkles } from "lucide-react" +import { useFormGroupIsSubmitting } from "@/hooks/use-form-group-is-submitting" + +export function InviteAcceptForm({ inviteToken }: { inviteToken: string }) { + const { formGroupIsSubmitting } = useFormGroupIsSubmitting() + + return ( + <> + +
+

or

+
+ + + + + Creating your own account will ignore this invitation. One account per + user. + + + + ) +} diff --git a/templates/one-to-many/src/components/invite-card.tsx b/templates/one-to-many/src/components/invite-card.tsx new file mode 100644 index 0000000..6131f5c --- /dev/null +++ b/templates/one-to-many/src/components/invite-card.tsx @@ -0,0 +1,70 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { CircleUser } from "lucide-react" +import type { InviteFull, Invite } from "@/lib/types" +import { AccountSignInForm } from "./account-signin-form" +import { InviteAcceptForm } from "./invite-accept-form" +import { FormGroupContextProvider } from "@/hooks/use-form-group-is-submitting" +export function InviteCard({ + type = "preview", + invite, +}: { + type: "preview" | "full" + invite: T +}) { + if (!invite) { + return null + } + + if (type === "preview") { + return ( + + + + + + + + + You've been invited to join {invite.account.name} + + + + + + + ) + } else if (type === "full") { + if (!("inviter" in invite)) { + throw new Error("type is full but invite is not InviteFull") + } + + return ( + + + + + + + + + + {invite.inviter.name} invited you to join {invite.account.name} + + + + + + + + + ) + } + + return null +} diff --git a/templates/one-to-many/src/components/invite-user-form.tsx b/templates/one-to-many/src/components/invite-user-form.tsx new file mode 100644 index 0000000..86733e6 --- /dev/null +++ b/templates/one-to-many/src/components/invite-user-form.tsx @@ -0,0 +1,112 @@ +"use client" + +import { useActionState, useEffect, useState } from "react" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { AlertCircle, CirclePlus, Loader2 } from "lucide-react" +import { doInviteCreate } from "@/actions/invite/do-invite-create" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import type { UUID } from "@/lib/types" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" + +export default function InviteUserForm({ accountId }: { accountId: UUID }) { + const [state, formAction, isPending] = useActionState( + doInviteCreate, + undefined, + ) + const [open, setOpen] = useState(false) + const [error, setError] = useState(undefined) + const [email, setEmail] = useState(undefined) + + const handleOpenChange = (isOpening: boolean) => { + // Prevents UI shift when closing the dialog that has an error + if (isOpening) { + setError(undefined) + } + + setOpen(isOpening) + } + + useEffect(() => { + if (state?.status === "success") { + setOpen(false) + } + + if (state?.status === "error") { + setError(state.messages?.[0]?.body) + } + + setEmail(state?.data?.email) + }, [state]) + + return ( + <> + + + + + + +
+ + + + Invite a new user to the team + + Entering an email address will send an invite to the user to + join this account. + + + {error && !isPending && ( + + + + {state?.messages?.[0]?.title} + + + {state?.messages?.[0]?.body} + + + )} +
+ + setEmail(e.target.value)} + disabled={isPending} + /> +
+ + + + + + +
+
+
+ + ) +} diff --git a/templates/one-to-many/src/components/layout/app-sidebar.tsx b/templates/one-to-many/src/components/layout/app-sidebar.tsx new file mode 100644 index 0000000..2e69d4a --- /dev/null +++ b/templates/one-to-many/src/components/layout/app-sidebar.tsx @@ -0,0 +1,107 @@ +"use client" + +import * as React from "react" +import { + Calendar, + Command, + Inbox, + LifeBuoy, + Send, + Settings, + Star, + Users, +} from "lucide-react" + +import { NavMain } from "@/components/layout/nav-main" +import { NavSecondary } from "@/components/layout/nav-secondary" +import { NavUser } from "@/components/layout/nav-user" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar" +import { type CurrentUser } from "@/lib/types" +import Link from "next/link" + +const data = { + navMain: [ + { + title: "Inbox", + url: "/app", + icon: Inbox, + }, + { + title: "Today", + url: "#", + icon: Star, + }, + { + title: "Upcoming", + url: "#", + icon: Calendar, + }, + { + title: "Team", + url: "/app/team", + icon: Users, + }, + { + title: "Settings", + url: "#", + icon: Settings, + }, + ], + navSecondary: [ + { + title: "Support", + url: "#", + icon: LifeBuoy, + }, + { + title: "Feedback", + url: "#", + icon: Send, + }, + ], +} + +export function AppSidebar({ + currentUser, + ...props +}: React.ComponentProps & { currentUser: CurrentUser }) { + return ( + + + + + + +
+ +
+
+ + {currentUser?.account?.name} + +
+ +
+
+
+
+ + + + + + + + +
+ ) +} diff --git a/templates/one-to-many/src/components/layout/email-invite.tsx b/templates/one-to-many/src/components/layout/email-invite.tsx new file mode 100644 index 0000000..663773b --- /dev/null +++ b/templates/one-to-many/src/components/layout/email-invite.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +interface EmailInviteProps { + context: { + token: string + account: { + name: string + } + } +} + +export const EmailInvite: React.FC> = ({ + context, +}) => ( +
+ Click here{" "} + to join {context.account.name}. +
+) diff --git a/templates/one-to-many/src/components/layout/loading-skeleton.tsx b/templates/one-to-many/src/components/layout/loading-skeleton.tsx new file mode 100644 index 0000000..8ff73e4 --- /dev/null +++ b/templates/one-to-many/src/components/layout/loading-skeleton.tsx @@ -0,0 +1,48 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default async function LoadingSkeleton({ + variant = "default", +}: { + variant?: "default" | "settings" +}) { + if (variant === "settings") { + return ( +
+ {/* Page header */} +
+ {/* Page title */} + {/* Description */} +
+ + {/* Settings sections */} +
+ {/* Section 1 */} +
+ {/* Section title */} +
+ {/* Setting row */} + {/* Setting row */} + {/* Setting row */} +
+
+ + {/* Section 2 */} +
+ {/* Section title */} +
+ {/* Setting row */} + {/* Setting row */} +
+
+
+
+ ) + } + + return ( +
+ + +
+ ) +} diff --git a/templates/one-to-many/src/components/layout/nav-main.tsx b/templates/one-to-many/src/components/layout/nav-main.tsx new file mode 100644 index 0000000..936910a --- /dev/null +++ b/templates/one-to-many/src/components/layout/nav-main.tsx @@ -0,0 +1,44 @@ +"use client" + +import { type LucideIcon } from "lucide-react" +import { + SidebarGroup, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" +import Link from "next/link" +import { usePathname } from "next/navigation" + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon: LucideIcon + }[] +}) { + const currentPath = usePathname() + + return ( + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + ) +} diff --git a/templates/one-to-many/src/components/layout/nav-secondary.tsx b/templates/one-to-many/src/components/layout/nav-secondary.tsx new file mode 100644 index 0000000..a931a7e --- /dev/null +++ b/templates/one-to-many/src/components/layout/nav-secondary.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { type LucideIcon } from "lucide-react" + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string + url: string + icon: LucideIcon + }[] +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ) +} diff --git a/templates/one-to-many/src/components/layout/nav-user.tsx b/templates/one-to-many/src/components/layout/nav-user.tsx new file mode 100644 index 0000000..ce0bdb6 --- /dev/null +++ b/templates/one-to-many/src/components/layout/nav-user.tsx @@ -0,0 +1,121 @@ +"use client" + +import { + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Settings2, + Sparkles, + CircleUser, +} from "lucide-react" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" +import { type CurrentUser } from "@/lib/types" +import Link from "next/link" + +export function NavUser({ user }: { user: CurrentUser }) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + + + + +
+ {user?.name} + {user?.email} +
+ +
+
+ + +
+ + + + + + +
+ {user?.name} + {user?.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Preferences + + + + Billing + + + + Notifications + + + + + + + Sign out + + +
+
+
+
+ ) +} diff --git a/templates/one-to-many/src/components/magic-sign-in-button.tsx b/templates/one-to-many/src/components/magic-sign-in-button.tsx new file mode 100644 index 0000000..b1157b3 --- /dev/null +++ b/templates/one-to-many/src/components/magic-sign-in-button.tsx @@ -0,0 +1,98 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { Loader2, CircleX, Sparkles } from "lucide-react" +import { useActionState, useEffect, useState } from "react" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useRouter } from "next/navigation" +import { doMagicAuth } from "@/actions/auth/do-magic-auth" +import { serverActionResponseSchema } from "@/lib/schemas" +import { useFormGroupIsSubmitting } from "@/hooks/use-form-group-is-submitting" + +export function MagicSignInButton({ callbackUrl }: { callbackUrl?: string }) { + const [state, formAction, isPending] = useActionState(doMagicAuth, undefined) + const [email, setEmail] = useState("") + const router = useRouter() + + // Validate the state response is what we're expecting + const validState = serverActionResponseSchema.safeParse(state) + + // Check if the state is valid + if (state !== undefined && !validState.success) { + throw new Error("Invalid state response from the server") + } + + // We want to keep the form disabled if the action is successful, because we're going to redirect the user and the form is not reusable. + const isDisabled = isPending || state?.status === "success" + + const { formGroupIsSubmitting, setFormGroupIsSubmitting } = + useFormGroupIsSubmitting() + + useEffect(() => { + if (isDisabled) { + setFormGroupIsSubmitting(isDisabled) + } + }, [isDisabled]) + + // Redirect the user to the verify page if the action is successful + useEffect(() => { + if (state?.status === "success") { + router.push("/verify") + return + } + }, [state]) + + return ( + <> +
+

or

+
+
+
+ + + setEmail(e.target.value)} + /> + {state?.messages && ( + <> + {state.messages.map((message, index) => ( + + + {message.title} + {message.body} + + ))} + + )} + + + + + We'll email you a magic link for a password-free sign in. + + +
+
+ + ) +} diff --git a/templates/one-to-many/src/components/remove-invite.tsx b/templates/one-to-many/src/components/remove-invite.tsx new file mode 100644 index 0000000..82cc603 --- /dev/null +++ b/templates/one-to-many/src/components/remove-invite.tsx @@ -0,0 +1,116 @@ +"use client" + +import { useActionState, useEffect, useState } from "react" +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Button } from "@/components/ui/button" +import { AlertCircle, Loader2, Trash } from "lucide-react" +import { doRemoveInvite } from "@/actions/invite/do-remove-invite" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import type { UUID } from "@/lib/types" + +export default function RemoveInvite({ + inviteId, + currentUserRole, +}: { + inviteId: UUID + currentUserRole: string +}) { + const [state, formAction, isPending] = useActionState( + doRemoveInvite, + undefined, + ) + const [open, setOpen] = useState(false) + const [error, setError] = useState(undefined) + + const handleOpenChange = (isOpening: boolean) => { + // Prevents UI shift when closing the dialog that has an error + if (isOpening) { + setError(undefined) + } + + setOpen(isOpening) + } + + const canRemoveInvite = () => { + if (currentUserRole === "owner") return true + if (currentUserRole === "admin") return true + if (currentUserRole === "user") return false + } + + useEffect(() => { + if (state?.status === "success") { + setOpen(false) + } + + if (state?.status === "error") { + setError(state.messages?.[0]?.body) + } + }, [state]) + + return ( + + + + + + + + + +

Remove this invite

+
+
+
+ +
+ + + Remove this invite? + + + This invite will be immediately removed, and won't be able to + be used to join the team. + + {error && !isPending && ( + + + + {state?.messages?.[0]?.title} + + {state?.messages?.[0]?.body} + + )} + + Cancel + + +
+
+
+ ) +} diff --git a/templates/one-to-many/src/components/remove-user.tsx b/templates/one-to-many/src/components/remove-user.tsx new file mode 100644 index 0000000..6c1fa1f --- /dev/null +++ b/templates/one-to-many/src/components/remove-user.tsx @@ -0,0 +1,118 @@ +"use client" + +import { useActionState, useEffect, useState } from "react" +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Button } from "@/components/ui/button" +import { AlertCircle, Loader2, Trash } from "lucide-react" +import { doRemoveUser } from "@/actions/account/do-remove-user" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import type { UUID } from "@/lib/types" +import { type AccountUsersWithInvites } from "@/actions/account/fetch-account-users-with-invites" + +export default function RemoveUser({ + user, + accountId, + currentUserRole, +}: { + user: AccountUsersWithInvites & { type: "user" } + accountId: UUID + currentUserRole: string +}) { + const [state, formAction, isPending] = useActionState(doRemoveUser, undefined) + const [open, setOpen] = useState(false) + const [error, setError] = useState(undefined) + + const handleOpenChange = (isOpening: boolean) => { + // Prevents UI shift when closing the dialog that has an error + if (isOpening) { + setError(undefined) + } + + setOpen(isOpening) + } + + const canRemoveUser = () => { + if (user.role === "owner") return false + if (currentUserRole === "owner") return true + if (currentUserRole === "admin" && user.role !== "admin") return true + if (currentUserRole === "user") return false + } + + useEffect(() => { + if (state?.status === "success") { + setOpen(false) + } + + if (state?.status === "error") { + setError(state.messages?.[0]?.body) + } + }, [state]) + + return ( + + + + + + + + + +

Remove user from team

+
+
+
+ +
+ + + + Remove {user.name} from team? + + + They will be immediately removed from the team, and will need to be + re-invited to join again. + + {error && !isPending && ( + + + + {state?.messages?.[0]?.title} + + {state?.messages?.[0]?.body} + + )} + + Cancel + + +
+
+
+ ) +} diff --git a/templates/one-to-many/src/components/signout.tsx b/templates/one-to-many/src/components/signout.tsx new file mode 100644 index 0000000..70619ce --- /dev/null +++ b/templates/one-to-many/src/components/signout.tsx @@ -0,0 +1,17 @@ +"use client" + +import { useEffect } from "react" +import { doSignout } from "@/actions/auth/do-signout" + +export function DoSignout() { + useEffect(() => { + // We don't want have /signout in the URL history + const signout = async () => { + await doSignout() + window.location.replace("/") + } + signout() + }, []) + + return null +} diff --git a/templates/one-to-many/src/components/social-sign-in-button.tsx b/templates/one-to-many/src/components/social-sign-in-button.tsx new file mode 100644 index 0000000..8827c8e --- /dev/null +++ b/templates/one-to-many/src/components/social-sign-in-button.tsx @@ -0,0 +1,48 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import { Loader2 } from "lucide-react" +import { useActionState, useEffect } from "react" +import { doSocialAuth } from "@/actions/auth/do-social-auth" +import { useFormGroupIsSubmitting } from "@/hooks/use-form-group-is-submitting" + +export function SocialSignInButton({ + providerName, + callbackUrl, + className, + children, + ...props +}: { + providerName: "google" + callbackUrl?: string + className?: string + children: React.ReactNode +}) { + const [, formAction, isPending] = useActionState(doSocialAuth, undefined) + + const { formGroupIsSubmitting, setFormGroupIsSubmitting } = + useFormGroupIsSubmitting() + + useEffect(() => { + if (isPending) { + setFormGroupIsSubmitting(isPending) + } + }, [isPending]) + + return ( +
+ + + +
+ ) +} diff --git a/templates/one-to-many/src/components/svg/google-logo.tsx b/templates/one-to-many/src/components/svg/google-logo.tsx new file mode 100644 index 0000000..b74f538 --- /dev/null +++ b/templates/one-to-many/src/components/svg/google-logo.tsx @@ -0,0 +1,44 @@ +import * as React from "react" + +const GoogleLogo = (props: React.SVGProps) => ( + + + + + + + + + + + +) +export default GoogleLogo diff --git a/templates/one-to-many/src/components/ui/alert-dialog.tsx b/templates/one-to-many/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..25e7b47 --- /dev/null +++ b/templates/one-to-many/src/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/templates/one-to-many/src/components/ui/alert.tsx b/templates/one-to-many/src/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/templates/one-to-many/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/templates/one-to-many/src/components/ui/avatar.tsx b/templates/one-to-many/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/templates/one-to-many/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/templates/one-to-many/src/components/ui/badge.tsx b/templates/one-to-many/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/templates/one-to-many/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/templates/one-to-many/src/components/ui/breadcrumb.tsx b/templates/one-to-many/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/templates/one-to-many/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>