Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: invalidate all older sessions #110

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UserProfileSummary from "@/components/ui/user-profile-summary";
import { getUsersForRepository } from "@/lib/repository/service";
import { getUsersForRepository } from "@/lib/user/service";

export const metadata = {
title: "Player overview",
Expand Down
41 changes: 41 additions & 0 deletions app/api/github-webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { registerHooks } from "@/lib/github";
import { EmitterWebhookEvent, EmitterWebhookEventName } from "@octokit/webhooks";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

// Set to store processed event IDs
const processedEvents = new Set<string>();

export async function POST(req: Request) {
const headersList = headers();
const eventId = headersList.get("x-github-delivery") as string;
const githubEvent = headersList.get("x-github-event") as string;

let body: EmitterWebhookEvent<"issue_comment" | "pull_request" | "installation">["payload"];

try {
body = await req.json();
} catch (error) {
return NextResponse.json({ error: "Invalid JSON payload" }, { status: 400 });
}

if (!eventId) {
return NextResponse.json({ error: "Missing X-GitHub-Delivery header" }, { status: 400 });
}

if (processedEvents.has(eventId)) {
return NextResponse.json({ message: `Event ${eventId} already processed, skipping` }, { status: 200 });
}

registerHooks(githubEvent as EmitterWebhookEventName, body);

processedEvents.add(eventId);
setTimeout(
() => {
processedEvents.delete(eventId);
},
24 * 60 * 60 * 1000
); // 24 hours

return NextResponse.json({ message: `Event ${eventId} processed` }, { status: 200 });
}
1 change: 0 additions & 1 deletion env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const env = createEnv({
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
// NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY: z.string().min(1).optional(),
},
runtimeEnv: {
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
Expand Down
9 changes: 9 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ export const authOptions: NextAuthOptions = {
clientSecret: env.GITHUB_APP_CLIENT_SECRET,
}),
],
cookies: {
sessionToken: {
name: "hackathon-2024.session-token",
// Looks like this isn't optional, but it will merge with the default
// cookies from nextauth
// https://github.com/nextauthjs/next-auth/blob/5e5a7fc5b41ea2e7e687f5c6e6d89c7967609dcb/packages/core/src/lib/utils/cookie.ts#L58
options: {},
}
},
callbacks: {
async signIn({ user, account, profile, ...rest }: any) {
if (account.type !== "oauth") {
Expand Down
33 changes: 33 additions & 0 deletions lib/enrollment/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { revalidateTag } from "next/cache";

interface RevalidateProps {
userId?: string;
repositoryId?: string;
}

export const enrollmentCache = {
tag: {
byUserId(userId: string) {
return `users-${userId}-enrollment`;
},
byRepositoryId(repositoryId: string) {
return `repositories-${repositoryId}-enrollment`;
},
byUserIdAndRepositoryId(userId: string, repositoryId: string) {
return `users-${userId}-repositories-${repositoryId}-enrollment`;
},
},
revalidate({ userId, repositoryId }: RevalidateProps): void {
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}

if (repositoryId) {
revalidateTag(this.tag.byRepositoryId(repositoryId));
}

if (userId && repositoryId) {
revalidateTag(this.tag.byUserIdAndRepositoryId(userId, repositoryId));
}
},
};
129 changes: 88 additions & 41 deletions lib/enrollment/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,35 @@ import { TEnrollment, TEnrollmentInput, ZEnrollmentInput } from "@/types/enrollm
import { DatabaseError } from "@/types/errors";
import { TRepository } from "@/types/repository";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";

import { validateInputs } from "../utils/validate";
import { enrollmentCache } from "./cache";

export const getEnrollment = async (userId: string, repositoryId: string) =>
unstable_cache(
async () => {
try {
const enrollment = await db.enrollment.findFirst({
where: {
userId,
repositoryId,
},
});

return enrollment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getEnrollment-${userId}-${repositoryId}`],
{
tags: [enrollmentCache.tag.byUserIdAndRepositoryId(userId, repositoryId)],
}
)();

/**
* Enrolls a user in all repositories.
Expand Down Expand Up @@ -48,12 +75,7 @@ export const createEnrollment = async (enrollmentData: TEnrollmentInput): Promis

try {
// Check if enrollment already exists
const existingEnrollment = await db.enrollment.findFirst({
where: {
userId: enrollmentData.userId,
repositoryId: enrollmentData.repositoryId,
},
});
const existingEnrollment = await getEnrollment(enrollmentData.userId, enrollmentData.repositoryId);

if (existingEnrollment) {
throw new Error("Enrollment already exists.");
Expand All @@ -63,6 +85,11 @@ export const createEnrollment = async (enrollmentData: TEnrollmentInput): Promis
data: enrollmentData,
});

enrollmentCache.revalidate({
userId: enrollmentData.userId,
repositoryId: enrollmentData.repositoryId,
});

return enrollment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
Expand All @@ -89,6 +116,11 @@ export const deleteEnrollment = async (userId: string, repositoryId: string): Pr
},
},
});

enrollmentCache.revalidate({
userId,
repositoryId,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
Expand All @@ -104,15 +136,23 @@ export const deleteEnrollment = async (userId: string, repositoryId: string): Pr
* @returns A boolean indicating whether the user is enrolled.
*/

export const hasEnrollmentForRepository = async (userId: string, repositoryId: string): Promise<boolean> => {
const count = await db.enrollment.count({
where: {
userId,
repositoryId,
export const hasEnrollmentForRepository = async (userId: string, repositoryId: string): Promise<boolean> =>
unstable_cache(
async () => {
const count = await db.enrollment.count({
where: {
userId,
repositoryId,
},
});

return count > 0;
},
});
return count > 0;
};
[`hasEnrollmentForRepository-${userId}-${repositoryId}`],
{
tags: [enrollmentCache.tag.byUserIdAndRepositoryId(userId, repositoryId)],
}
)();

/**
* Retrieves an array of repositories that a user is enrolled in.
Expand All @@ -121,32 +161,39 @@ export const hasEnrollmentForRepository = async (userId: string, repositoryId: s
* a repository the user is enrolled in. The array is empty if the user has no enrollments.
*/

export const getEnrolledRepositories = async (userId: string): Promise<TRepository[]> => {
const enrolledRepositories = await db.repository.findMany({
where: {
enrollments: {
some: {
userId: userId,
export const getEnrolledRepositories = async (userId: string): Promise<TRepository[]> =>
unstable_cache(
async () => {
const enrolledRepositories = await db.repository.findMany({
where: {
enrollments: {
some: {
userId: userId,
},
},
},
},
},
select: {
id: true,
githubId: true,
name: true,
description: true,
homepage: true,
configured: true,
topics: true,
installation: true,
installationId: true,
pointTransactions: true,
enrollments: true,
logoUrl: true,
levels: true,
owner: true,
},
});
select: {
id: true,
githubId: true,
name: true,
description: true,
homepage: true,
configured: true,
topics: true,
installation: true,
installationId: true,
pointTransactions: true,
enrollments: true,
logoUrl: true,
levels: true,
owner: true,
},
});

return enrolledRepositories;
};
return enrolledRepositories;
},
[`getEnrolledRepositories-${userId}`],
{
tags: [enrollmentCache.tag.byUserId(userId)],
}
)();
25 changes: 25 additions & 0 deletions lib/github/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { revalidateTag } from "next/cache";

interface RevalidateProps {
repoGithubId?: number;
githubLogin?: string;
}

export const githubCache = {
tag: {
byRepoGithubId(repoGithubId: number) {
return `github-repo-${repoGithubId}`;
},
byGithubLogin(githubLogin: string) {
return `github-repo-${githubLogin}`;
},
},
revalidate({ repoGithubId, githubLogin }: RevalidateProps): void {
if (repoGithubId) {
revalidateTag(this.tag.byRepoGithubId(repoGithubId));
}
if (githubLogin) {
revalidateTag(this.tag.byGithubLogin(githubLogin));
}
},
};
Loading
Loading