Skip to content

Commit

Permalink
feat: clerk authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrastopoulos committed Feb 13, 2024
1 parent 92fba18 commit 9f3e388
Show file tree
Hide file tree
Showing 31 changed files with 799 additions and 703 deletions.
10 changes: 3 additions & 7 deletions .env-example → .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
# Prisma
DATABASE_URL=

# Next Auth
NEXTAUTH_SECRET=
NEXTAUTH_URL=

# Next Auth Google Provider
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Auth
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
1 change: 1 addition & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
17 changes: 7 additions & 10 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ function defineNextConfig(config) {
export default defineNextConfig({
reactStrictMode: true,
swcMinify: true,
// Next.js i18n docs: https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en'],
defaultLocale: 'en'
},
images: {
domains: [
'drive.google.com',
'www.google.com',
'www.cs.cmu.edu',
'lh3.googleusercontent.com'
remotePatterns: [
{
protocol: 'https',
hostname: '**',
port: '',
pathname: '**'
}
]
}
});
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"format": "yarn prettier . --write --ignore-path ./.gitignore"
},
"dependencies": {
"@clerk/nextjs": "^4.29.7",
"@headlessui/react": "^1.7.13",
"@headlessui/tailwindcss": "^0.1.3",
"@hookform/resolvers": "^2.9.11",
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.12.0",
"@tailwindcss/typography": "^0.5.7",
"@tanstack/react-query": "^4.24.6",
Expand All @@ -25,8 +25,7 @@
"clsx": "^1.2.1",
"daisyui": "^2.39.1",
"mongodb": "^4.11.0",
"next": "^13.2.1",
"next-auth": "^4.19.2",
"next": "14",
"next-themes": "^0.2.1",
"papaparse": "^5.3.2",
"pg": "^8.8.0",
Expand Down
40 changes: 5 additions & 35 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ model Item {
model AuditLog {
id String @id @default(cuid())
interaction ItemInteraction
actor User @relation(fields: [actorId], references: [id], onDelete: Cascade)
actor Account @relation(fields: [actorId], references: [clerkId], onDelete: Cascade)
actorId String
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
itemId String
Expand All @@ -124,51 +124,21 @@ model AuditLog {
model Subscription {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user Account @relation(fields: [userId], references: [clerkId], onDelete: Cascade)
category Category
createdAt DateTime @default(now())
@@unique([userId, category])
}

model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}

model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
clerkId String @id @unique
notifications Boolean @default(true)
permission Permission @default(USER)
accounts Account[]
sessions Session[]
auditLogs AuditLog[]
subscriptions Subscription[]
@@index([clerkId])
}

model VerificationToken {
Expand Down
45 changes: 45 additions & 0 deletions src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getAuth } from '@clerk/nextjs/server';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { NextRequest } from 'next/server';
import { createTRPCContext } from 'server/trpc/context';
import { appRouter } from 'server/trpc/router/_app';

/**
* Configure basic CORS headers
* You should extend this to match your needs
*/
function setCorsHeaders(res: Response) {
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('Access-Control-Request-Method', '*');
res.headers.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
res.headers.set('Access-Control-Allow-Headers', '*');
}

export function OPTIONS() {
const response = new Response(null, {
status: 204
});
setCorsHeaders(response);
return response;
}

const handler = async (req: NextRequest) => {
const response = await fetchRequestHandler({
endpoint: '/api/trpc',
router: appRouter,
req,
createContext: () =>
createTRPCContext({
session: getAuth(req),
headers: req.headers
}),
onError({ error, path }) {
console.error(`>>> tRPC Error on '${path}'`, error);
}
});

setCorsHeaders(response);
return response;
};

export { handler as GET, handler as POST };
53 changes: 31 additions & 22 deletions src/components/AuthWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SignInButton, SignOutButton, useSignIn, useUser } from '@clerk/nextjs';
import { Menu, Transition } from '@headlessui/react';
import { Permission } from '@prisma/client';
import { signIn, signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
import { Fragment } from 'react';
import {
Expand All @@ -14,32 +14,40 @@ import {
FaUserGraduate
} from 'react-icons/fa';
import useDialogStore from 'stores/DialogStore';
import { trpc } from 'utils/trpc';

function AuthWidget() {
const { data: session, status } = useSession();
const { isLoaded: isUserLoaded, isSignedIn, user } = useUser();
const { isLoaded: isSignInLoaded } = useSignIn();
const { subscribeDialog, manageSubscriptionsDialog } = useDialogStore();
const { data, isSuccess } = trpc.user.me.useQuery();

if (status === 'loading') {
if (!isUserLoaded || !isSignInLoaded || !isSuccess) {
return (
<div className="btn-disabled btn-ghost btn-circle btn">
<FaCircleNotch className="animate-spin text-white md:text-black" />;
</div>
);
}

if (!session) {
if (!isSignedIn) {
return (
<button
type="button"
className="btn-ghost btn-sm btn gap-2 md:btn-primary"
onClick={() => signIn('google')}
>
<FaSignInAlt />
<span>Sign in</span>
</button>
<SignInButton>
<button
type="button"
className="btn-ghost btn-sm btn gap-2 md:btn-primary"
>
<FaSignInAlt />
<span>Sign in</span>
</button>
</SignInButton>
);
}

if (!data) {
return <FaCircleNotch className="animate-spin text-white md:text-black" />;
}

return (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button tabIndex={0} className="flex-0 btn-ghost btn-circle btn">
Expand All @@ -58,7 +66,7 @@ function AuthWidget() {
unmount={false}
className="absolute right-0 z-50 w-40 origin-top-right rounded-md bg-base-100 p-4 text-base-content shadow-2xl ring-1 ring-black ring-opacity-5"
>
{session.user.permission === Permission.ADMIN && (
{data.permission === Permission.ADMIN && (
<>
<Menu.Item>
<Link
Expand All @@ -82,7 +90,7 @@ function AuthWidget() {
</>
)}

{session.user.permission === Permission.MODERATOR && (
{data.permission === Permission.MODERATOR && (
<>
<Menu.Item>
<Link
Expand Down Expand Up @@ -119,14 +127,15 @@ function AuthWidget() {
</Menu.Item>
<div className="divider my-1" />
<Menu.Item>
<button
type="button"
className="flex w-full items-center rounded-md px-2 py-2 text-sm ui-active:bg-accent ui-active:text-accent-content"
onClick={() => signOut()}
>
<FaSignOutAlt className="mr-2 h-4 w-4" aria-hidden="true" />
<span>Sign out</span>
</button>
<SignOutButton>
<button
type="button"
className="flex w-full items-center rounded-md px-2 py-2 text-sm ui-active:bg-accent ui-active:text-accent-content"
>
<FaSignOutAlt className="mr-2 h-4 w-4" aria-hidden="true" />
<span>Sign out</span>
</button>
</SignOutButton>
</Menu.Item>
</Menu.Items>
</Transition>
Expand Down
14 changes: 6 additions & 8 deletions src/components/Dialogs/SubscriptionDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useUser } from '@clerk/nextjs';
import { Category } from '@prisma/client';
import useZodForm from 'hooks/useZodForm';
import { useSession } from 'next-auth/react';
import Image from 'next/image';
import { FaTimes } from 'react-icons/fa';
import { toast } from 'react-toastify';
Expand All @@ -15,9 +15,8 @@ function SubscriptionsForm() {
const context = trpc.useContext();

const { data: subscriptions, status } = trpc.subscription.list.useQuery();
const { data: session, status: sessionStatus } = useSession({
required: true
});

const { isLoaded, isSignedIn, user } = useUser();

const subscriptionCreate = trpc.subscription.create.useMutation({
onSuccess: () => {
Expand Down Expand Up @@ -48,9 +47,8 @@ function SubscriptionsForm() {
})
});

if (status === 'error' || sessionStatus === 'loading')
return <div>Failed to load</div>;
if (status === 'loading') return <div>Loading...</div>;
if (status === 'error' || !isSignedIn) return <div>Failed to load</div>;
if (status === 'loading' || !isLoaded) return <div>Loading...</div>;

return (
<form
Expand All @@ -69,7 +67,7 @@ function SubscriptionsForm() {
type="email"
className="input-bordered input-primary input w-full"
disabled
value={session.user.email ?? ''}
value={user.emailAddresses[0]?.emailAddress}
/>
</div>
<div className="form-control gap-1">
Expand Down
11 changes: 5 additions & 6 deletions src/components/Dialogs/UserEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@ function EditUserForm() {
onSuccess: (res) => {
context.user.search.invalidate();
setSelectedUser(null);
toast.success(`Updated ${res.name}`);
// clearDialog();
toast.success(`Updated account`);
},
onError: (err) => {
toast.error(err.message);
}
});
const methods = useZodForm({
schema: UserSchema.partial().omit({ email: true, name: true })
schema: UserSchema.partial()
});
useEffect(() => {
if (!selectedUser) return;
methods.reset({
permission: selectedUser.permission,
notifications: selectedUser.notifications
permission: selectedUser.account.permission,
notifications: selectedUser.account.notifications
});
}, [selectedUser]);
const { clearDialog } = useDialogStore();
Expand All @@ -41,7 +40,7 @@ function EditUserForm() {
<form
onSubmit={methods.handleSubmit((data) => {
userUpdateMutation.mutate({
id: selectedUser.id,
clerkId: selectedUser.account.clerkId,
data
});
}, console.error)}
Expand Down
Loading

0 comments on commit 9f3e388

Please sign in to comment.