Skip to content
Open

Add sso #1652

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
669 changes: 669 additions & 0 deletions src/app/create-account/complete-profile/page.tsx

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions src/app/create-account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export default function CreateAccount() {
setStep("enterCode")
window.scrollTo({
top: 0,
behavior: "smooth", // smooth scrolling
behavior: "smooth",
})
} catch (error) {
console.error("Error sending OTP", error)
Expand All @@ -194,7 +194,10 @@ export default function CreateAccount() {

const onSubmit = async (values: FormSchema) => {
if (!isLoaded) return

window.scrollTo({
top: 0,
behavior: "smooth",
})
setStep("verifying")
if (process.env.NEXT_PUBLIC_VERCEL_ENV === "production") track("created-account")

Expand Down Expand Up @@ -244,7 +247,7 @@ export default function CreateAccount() {
toast({
variant: "destructive",
title: "Failed to create user",
description: `An error occurred while trying to create user. ${(error as { message?: string })?.message ?? ""}.`,
description: `An error occurred while trying to create user. ${(error as { message?: string })?.message ?? ""}`,
})
setStep("enterCode")
}
Expand Down
66 changes: 49 additions & 17 deletions src/app/join/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"use client"

import { useSignIn } from "@clerk/nextjs"
import { SignIn } from "@clerk/nextjs"
import { type EmailLinkFactor } from "@clerk/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { track } from "@vercel/analytics/react"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { siGoogle } from "simple-icons"
import * as z from "zod"

import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"
Expand All @@ -17,6 +20,7 @@ import { toast } from "~/components/ui/use-toast"
import type { ClerkError } from "~/lib/types"
import { api } from "~/trpc/react"

const googleIcon = { path: siGoogle.path, title: siGoogle.title }
const formSchema = z.object({
email: z
.string()
Expand Down Expand Up @@ -127,6 +131,25 @@ export default function Join() {
setStep("email")
}
}
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing blank line before function declaration. Add a blank line between the closing brace and the function declaration for consistency with the codebase style.

Suggested change
}
}

Copilot uses AI. Check for mistakes.
const handleGoogleSignIn = async () => {
if (!isLoaded) return

try {
// Start the Google OAuth flow
await signIn.authenticateWithRedirect({
strategy: "oauth_google",
redirectUrl: `${window.location.origin}/sso-callback`,
redirectUrlComplete: `/dashboard`, // after everything succeeds
})
} catch (error) {
console.error("Google sign-in failed", error)
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the complete error object to console may expose sensitive information. Consider logging only the error message or using a structured logging approach that filters sensitive data.

Suggested change
console.error("Google sign-in failed", error)
console.error("Google sign-in failed:", (error as { message?: string })?.message ?? error)

Copilot uses AI. Check for mistakes.
toast({
variant: "destructive",
title: "Google sign-in failed",
description: `${(error as { message?: string })?.message ?? ""}`,
})
}
}

return (
<div>
Expand Down Expand Up @@ -156,24 +179,33 @@ export default function Join() {
</Alert>
)}
{step === "initial" ? (
<form onSubmit={form.handleSubmit(sendOtp)} className="space-y-4 mt-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="font-mono">Email address</FormLabel>
<FormControl>
<Input type="email" placeholder="hello@codersforcauses.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
Continue
<>
<form onSubmit={form.handleSubmit(sendOtp)} className="space-y-4 mt-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="font-mono">Email address</FormLabel>
<FormControl>
<Input type="email" placeholder="hello@codersforcauses.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
Continue
</Button>
</form>
<Button variant="outline" className="w-full mt-4" onClick={handleGoogleSignIn}>
<svg role="img" viewBox="0 0 24 24" height={16} width={16} className="fill-current mr-4">
<title>{googleIcon.title}</title>
<path d={googleIcon.path} />
</svg>
Continue with Google
</Button>
</form>
</>
) : (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 mt-4">
<FormLabel className="font-mono">Enter one-time code from your email</FormLabel>
Expand Down
2 changes: 1 addition & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button } from "~/components/ui/button"

export default function NotFound() {
return (
<main className="main container flex flex-col items-center justify-center gap-y-4">
<main className="main container flex flex-col items-center justify-center gap-y-4 p-8">
<title>Not Found | Coders for Causes</title>
<div className="select-none text-6xl md:text-8xl">¯\_(ツ)_/¯</div>
<h2 className="text-3xl font-bold">404: Not Found</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface ProfilePageProps {
currentUser: RouterOutputs["users"]["getCurrent"]
}

const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
const ProfilePageContent = ({ id, currentUser }: ProfilePageProps) => {
const [isEditing, setIsEditing] = useState(false)
const { data: user, refetch } = api.users.get.useQuery(id)

Expand Down Expand Up @@ -128,4 +128,4 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => {
return <ProfilePageSkeleton />
}

export default ProfilePage
export default ProfilePageContent
4 changes: 2 additions & 2 deletions src/app/profile/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import NotFound from "~/app/not-found"
import { api } from "~/trpc/server"

import ProfilePage from "./profile/page"
import ProfilePageContent from "./page-content"

const Profile = async ({ params: { id } }: { params: { id: string } }) => {
const currentUser = await api.users.getCurrent.query()

if (currentUser) {
return <ProfilePage currentUser={currentUser} id={id} />
return <ProfilePageContent currentUser={currentUser} id={id} />
}

return <NotFound />
Expand Down
10 changes: 10 additions & 0 deletions src/app/sso-callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AuthenticateWithRedirectCallback } from "@clerk/nextjs"

export default function Page() {
return (
<>
<div className="main container grid place-items-center text-xl">Signing you in...</div>
<AuthenticateWithRedirectCallback signUpFallbackRedirectUrl="/create-account/complete-profile" />
</>
)
}
34 changes: 19 additions & 15 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@ import { User } from "./server/db/schema"
const adminRoles = ["admin", "committee"]

const isAdminPage = createRouteMatcher(["/dashboard/admin(.*)"])
const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile/settings(.*)"])
const isAuthPage = createRouteMatcher(["/join(.*)"])
const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile(.*)"])
const isAuthPage = createRouteMatcher(["/join(.*)", "/sso-callback(.*)"])

export default clerkMiddleware(async (auth, req) => {
const clerkId = auth().userId
const { userId, redirectToSignIn } = auth()

if (isAdminPage(req) && clerkId) {
const user = await db.query.User.findFirst({
where: eq(User.clerk_id, clerkId),
})

if (!adminRoles.includes(user?.role ?? "")) {
// non-existent clerk role so we go to 404 page cleanly
auth().protect({
role: "lmfaooo",
if (isAdminPage(req)) {
if (userId) {
const user = await db.query.User.findFirst({
where: eq(User.clerk_id, userId),
})

if (!adminRoles.includes(user?.role ?? "")) {
// non-existent clerk role so we go to 404 page cleanly
auth().protect({
role: "lmfaooo",
})
}
} else {
return redirectToSignIn()
}
}

if (isProtectedPage(req)) {
auth().protect()
if (isProtectedPage(req) && !userId) {
return redirectToSignIn()
}

if (isAuthPage(req) && clerkId) {
if (isAuthPage(req) && userId) {
return Response.redirect(new URL("/dashboard", req.url))
}
})
Expand Down
14 changes: 14 additions & 0 deletions src/server/api/routers/users/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ export const get = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s"))

return user
})

export const getByEmail = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s"))
.input(z.string().email())
.query(async ({ ctx, input }) => {
const user = await ctx.db.query.User.findFirst({
where: eq(User.email, input),
})

if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input} does not exist` })
}

return user
})
3 changes: 2 additions & 1 deletion src/server/api/routers/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { env } from "~/env"
import { createTRPCRouter } from "~/server/api/trpc"

import { create } from "./create"
import { get } from "./get"
import { get, getByEmail } from "./get"
import { getCurrent } from "./get-current"
import { update } from "./update"
import { updateSocials } from "./update-socials"
Expand All @@ -15,4 +15,5 @@ export const usersRouter = createTRPCRouter({
get,
update,
updateSocials,
getByEmail,
})