Skip to content

Commit

Permalink
refactor session management based on the posts example
Browse files Browse the repository at this point in the history
  • Loading branch information
noxify committed Jan 19, 2024
1 parent f7ad286 commit d3d86da
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client"

import { use } from "react"
import { userAgentFromString } from "next/server"
import { MonitorIcon, SmartphoneIcon, Trash2Icon } from "lucide-react"

import type { RouterOutputs } from "@acme/api"
import { cn } from "@acme/ui"
import { Button } from "@acme/ui/button"
import { toast } from "@acme/ui/toast"

import { api } from "@/trpc/react"

export function SessionList(props: {
sessions: Promise<RouterOutputs["user"]["sessions"]>
sessionId: string
}) {
// TODO: Make `useSuspenseQuery` work without having to pass a promise from RSC
const initialData = use(props.sessions)
const { data: sessions } = api.user.sessions.useQuery(undefined, {
initialData,
})

if (sessions.length === 0) {
return (
<div className="relative flex w-full flex-col gap-4">
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/10">
<p className="text-2xl font-bold text-white">No sessions found</p>
</div>
</div>
)
}

return (
<>
{sessions.map((session) => {
return (
<SessionCard
key={session.id}
session={session}
sessionId={props.sessionId}
/>
)
})}
</>
)
}

export function SessionCard(props: {
session: RouterOutputs["user"]["sessions"][number]
sessionId: string
}) {
const utils = api.useUtils()
const deleteSession = api.user.deleteSession.useMutation({
onSuccess: async () => {
await utils.user.invalidate()
},
onError: (err) => {
toast.error(
err?.data?.code === "UNAUTHORIZED"
? "You must be logged in to manage your session"
: "Failed to delete selected session",
)
},
})

const ua = userAgentFromString(props.session.userAgent ?? "")
const isMobile = () =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
props.session.userAgent ?? "",
)

return (
<div className=" flex w-full items-center space-x-4 rounded-md border p-4">
{isMobile() ? <SmartphoneIcon /> : <MonitorIcon />}
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
{ua.os.name} - {ua.browser.name}
</p>
<p className="text-sm text-muted-foreground">
{props.session.ipAddress === "::1"
? "127.0.0.1 (localhost)"
: props.session.ipAddress}{" "}
{props.session.id === props.sessionId && " - Current session"}
</p>
</div>
{props.session.id !== props.sessionId && (
<Button
size="icon"
variant="destructive"
onClick={() => {
deleteSession.mutate({ sessionId: props.session.id })
}}
>
<Trash2Icon className="h-4 w-4" />
</Button>
)}
</div>
)
}
57 changes: 17 additions & 40 deletions apps/nextjs/src/app/(authorized)/users/settings/sessions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Suspense } from "react"
import { redirect } from "next/navigation"
import { userAgentFromString } from "next/server"
import { MonitorIcon, SmartphoneIcon, Trash2Icon } from "lucide-react"

import { auth, lucia } from "@acme/auth"
import { auth } from "@acme/auth"
import { Button } from "@acme/ui/button"
import { Skeleton } from "@acme/ui/skeleton"

import { invalivateSessionAction } from "@/actions/sessions"
import { SessionList } from "@/app/(authorized)/users/settings/components/sessions"
import { api } from "@/trpc/server"

export default async function SessionsPage() {
const session = await auth()
Expand All @@ -14,49 +18,22 @@ export default async function SessionsPage() {
redirect("/auth")
}

const sessions = await lucia.getUserSessions(session.user?.id)
const sessions = api.user.sessions()

return (
<>
<div className="grid w-full gap-y-2">
{sessions.map((ele, index) => {
const ua = userAgentFromString(ele.userAgent ?? "")
const isMobile = () =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
ele.userAgent ?? "",
)

return (
<div
className=" flex w-full items-center space-x-4 rounded-md border p-4"
key={index}
>
{isMobile() ? <SmartphoneIcon /> : <MonitorIcon />}
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
{ua.os.name} - {ua.browser.name}
</p>
<p className="text-sm text-muted-foreground">
{ele.ipAddress === "::1"
? "127.0.0.1 (localhost)"
: ele.ipAddress}{" "}
{ele.id === session.session.id && " - Current session"}
</p>
</div>
{ele.id !== session.session.id && (
<Button
size="icon"
variant="destructive"
onClick={async () => {
"use server"
await invalivateSessionAction(ele.id)
}}
>
<Trash2Icon className="h-4 w-4" />
</Button>
)}
<Suspense
fallback={
<div className="flex w-full flex-col gap-4">
<Skeleton />
<Skeleton />
<Skeleton />
</div>
)
})}
}
>
<SessionList sessions={sessions} sessionId={session.session.id} />
</Suspense>
</div>
</>
)
Expand Down
2 changes: 0 additions & 2 deletions apps/nextjs/src/app/api/auth/[provider]/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export async function GET(
const state = searchParams.get("state")
const storedState = cookies().get(`oauth_state`)?.value ?? null
if (!code || !state || !storedState || state !== storedState) {
console.log("invalid code / state / storedState ", params.provider)

return new Response(null, {
status: 400,
})
Expand Down
21 changes: 20 additions & 1 deletion packages/api/src/router/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { TRPCError } from "@trpc/server"

import { lucia } from "@acme/auth"
import { eq, schema } from "@acme/db"
import { UpdateProfileSchema } from "@acme/validators"
import { DeleteSessionSchema, UpdateProfileSchema } from "@acme/validators"

import { createTRPCRouter, protectedProcedure } from "../trpc"

Expand All @@ -17,4 +20,20 @@ export const userRouter = createTRPCRouter({
.set(input)
.where(eq(schema.users.id, ctx.session.user.id))
}),

sessions: protectedProcedure.query(({ ctx }) => {
return lucia.getUserSessions(ctx.session.user?.id)
}),

deleteSession: protectedProcedure
.input(DeleteSessionSchema)
.mutation(async ({ ctx, input }) => {
const userSessions = await lucia.getUserSessions(ctx.session.user.id)

if (userSessions.find((ele) => ele.id === input.sessionId)) {
return await lucia.invalidateSession(input.sessionId)
} else {
throw new TRPCError({ code: "BAD_REQUEST" })
}
}),
})
5 changes: 2 additions & 3 deletions packages/api/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ export const createTRPCContext = async (opts: {
session: AuthResponse | null
}) => {
const session = opts.session ?? (await auth())
const source = opts.headers.get("x-trpc-source") ?? "unknown"

console.log(">>> tRPC Request from", source, "by", session?.user)
//const source = opts.headers.get("x-trpc-source") ?? "unknown"
// console.log(">>> tRPC Request from", source, "by", session?.user)

return {
session,
Expand Down
4 changes: 4 additions & 0 deletions packages/validators/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export const CreatePostSchema = z.object({
export const UpdateProfileSchema = z.object({
name: z.string().min(1),
})

export const DeleteSessionSchema = z.object({
sessionId: z.string().min(1),
})

0 comments on commit d3d86da

Please sign in to comment.