mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +00:00
add invite system and google oauth provider (#185)
* add settings page with members list * add invite to schema and basic create form * add invite table * add basic invite link copy button * add auth invite accept case * add non auth logic * add google oauth provider
This commit is contained in:
parent
846d73b0e6
commit
90550181af
18 changed files with 575 additions and 4 deletions
|
|
@ -0,0 +1,19 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Invite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"recipientEmail" TEXT NOT NULL,
|
||||
"hostUserId" TEXT NOT NULL,
|
||||
"orgId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Invite_recipientEmail_orgId_key" ON "Invite"("recipientEmail", "orgId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_hostUserId_fkey" FOREIGN KEY ("hostUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -83,6 +83,27 @@ model RepoToConnection {
|
|||
@@id([connectionId, repoId])
|
||||
}
|
||||
|
||||
model Invite {
|
||||
/// The globally unique invite id
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// Time of invite creation
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
/// The email of the recipient of the invite
|
||||
recipientEmail String
|
||||
|
||||
/// The user that created the invite
|
||||
host User @relation(fields: [hostUserId], references: [id], onDelete: Cascade)
|
||||
hostUserId String
|
||||
|
||||
/// The organization the invite is for
|
||||
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
orgId Int
|
||||
|
||||
@@unique([recipientEmail, orgId])
|
||||
}
|
||||
|
||||
model Org {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
|
@ -92,6 +113,9 @@ model Org {
|
|||
connections Connection[]
|
||||
repos Repo[]
|
||||
secrets Secret[]
|
||||
|
||||
/// List of pending invites to this organization
|
||||
invites Invite[]
|
||||
}
|
||||
|
||||
enum OrgRole {
|
||||
|
|
@ -139,6 +163,9 @@ model User {
|
|||
orgs UserToOrg[]
|
||||
activeOrgId Int?
|
||||
|
||||
/// List of pending invites that the user has created
|
||||
invites Invite[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
|||
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { encrypt } from "@sourcebot/crypto"
|
||||
import { getConnection } from "./data/connection";
|
||||
import { Prisma } from "@sourcebot/db";
|
||||
import { Prisma, Invite } from "@sourcebot/db";
|
||||
|
||||
const ajv = new Ajv({
|
||||
validateFormats: false,
|
||||
|
|
@ -301,3 +301,58 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
|
|||
|
||||
return parsedConfig;
|
||||
}
|
||||
|
||||
export const createInvite = async (email: string, userId: string, orgId: number): Promise<{ success: boolean } | ServiceError> => {
|
||||
console.log("Creating invite for", email, userId, orgId);
|
||||
|
||||
try {
|
||||
await prisma.invite.create({
|
||||
data: {
|
||||
recipientEmail: email,
|
||||
hostUserId: userId,
|
||||
orgId,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create invite:", error);
|
||||
return unexpectedError("Failed to create invite");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const redeemInvite = async (invite: Invite, userId: string): Promise<{ orgId: number } | ServiceError> => {
|
||||
try {
|
||||
await prisma.userToOrg.create({
|
||||
data: {
|
||||
userId,
|
||||
orgId: invite.orgId,
|
||||
role: "MEMBER",
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
activeOrgId: invite.orgId,
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.invite.delete({
|
||||
where: {
|
||||
id: invite.id,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
orgId: invite.orgId,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to redeem invite:", error);
|
||||
return unexpectedError("Failed to redeem invite");
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,13 @@ export const NavigationMenu = async () => {
|
|||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/settings" legacyBehavior passHref>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
Settings
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenuBase>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { auth } from "@/auth";
|
|||
import { getUser } from "@/data/user";
|
||||
import { prisma } from "@/prisma";
|
||||
import { ConnectionList } from "./components/connectionList";
|
||||
import { Header } from "./components/header";
|
||||
import { Header } from "../components/header";
|
||||
import { NewConnectionCard } from "./components/newConnectionCard";
|
||||
|
||||
export default async function ConnectionsPage() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { redeemInvite } from "../../../actions";
|
||||
import { isServiceError } from "@/lib/utils"
|
||||
import { useToast } from "@/components/hooks/use-toast"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Invite } from "@sourcebot/db"
|
||||
|
||||
interface AcceptInviteButtonProps {
|
||||
invite: Invite
|
||||
userId: string
|
||||
}
|
||||
|
||||
export function AcceptInviteButton({ invite, userId }: AcceptInviteButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleAcceptInvite = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await redeemInvite(invite, userId)
|
||||
if (isServiceError(res)) {
|
||||
console.log("Failed to redeem invite: ", res)
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to redeem invite. Please try again.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} else {
|
||||
router.push("/")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error redeeming invite:", error)
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "An unexpected error occurred. Please try again.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={handleAcceptInvite} disabled={isLoading}>
|
||||
{isLoading ? "Accepting..." : "Accept Invite"}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
84
packages/web/src/app/redeem/page.tsx
Normal file
84
packages/web/src/app/redeem/page.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { prisma } from "@/prisma";
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { NavigationMenu } from "../components/navigationMenu";
|
||||
import { auth } from "@/auth";
|
||||
import { getUser } from "@/data/user";
|
||||
import { AcceptInviteButton } from "./components/acceptInviteButton"
|
||||
|
||||
interface RedeemPageProps {
|
||||
searchParams?: {
|
||||
invite_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||
const invite_id = searchParams?.invite_id;
|
||||
|
||||
if (!invite_id) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { id: invite_id },
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>This invite either expired or was revoked. Contact your organization owner.</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
let user = undefined;
|
||||
if (session) {
|
||||
user = await getUser(session.user.id);
|
||||
}
|
||||
|
||||
|
||||
// Auth case
|
||||
if (user) {
|
||||
if (user.email !== invite.recipientEmail) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>Sorry this invite does not belong to you.</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
const orgName = await prisma.org.findUnique({
|
||||
where: { id: invite.orgId },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
if (!orgName) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>Organization not found. Please contact the invite sender.</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div className="flex justify-between items-center h-screen px-6">
|
||||
<h1 className="text-2xl font-bold">You've been invited to org {orgName.name}</h1>
|
||||
<AcceptInviteButton invite={invite} userId={user.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
redirect(`/login?callbackUrl=${encodeURIComponent(`/redeem?invite_id=${invite_id}`)}`);
|
||||
}
|
||||
}
|
||||
40
packages/web/src/app/settings/components/inviteTable.tsx
Normal file
40
packages/web/src/app/settings/components/inviteTable.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
'use client';
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { User } from "@sourcebot/db";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { InviteColumnInfo, inviteTableColumns } from "./inviteTableColumns"
|
||||
|
||||
export interface InviteInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface InviteTableProps {
|
||||
initialInvites: InviteInfo[];
|
||||
}
|
||||
|
||||
export const InviteTable = ({ initialInvites }: InviteTableProps) => {
|
||||
const [invites, setInvites] = useState<InviteInfo[]>(initialInvites);
|
||||
|
||||
const inviteRows: InviteColumnInfo[] = useMemo(() => {
|
||||
return invites.map(invite => {
|
||||
return {
|
||||
id: invite.id!,
|
||||
email: invite.email!,
|
||||
createdAt: invite.createdAt!,
|
||||
}
|
||||
})
|
||||
}, [invites]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTable
|
||||
columns={inviteTableColumns()}
|
||||
data={inviteRows}
|
||||
searchKey="email"
|
||||
searchPlaceholder="Search invites..."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { resolveServerPath } from "../../api/(client)/client";
|
||||
import { createPathWithQueryParams } from "@/lib/utils";
|
||||
|
||||
export type InviteColumnInfo = {
|
||||
id: string;
|
||||
email: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export const inviteTableColumns = (): ColumnDef<InviteColumnInfo>[] => {
|
||||
return [
|
||||
{
|
||||
accessorKey: "email",
|
||||
cell: ({ row }) => {
|
||||
const invite = row.original;
|
||||
return <div>{invite.email}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => {
|
||||
const invite = row.original;
|
||||
return invite.createdAt.toISOString();
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "copy",
|
||||
cell: ({ row }) => {
|
||||
const invite = row.original;
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
const basePath = `${window.location.origin}${resolveServerPath('/')}`;
|
||||
const url = createPathWithQueryParams(`${basePath}redeem?invite_id=${invite.id}`);
|
||||
navigator.clipboard.writeText(url);
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
'use client'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { createInvite } from "../../../actions"
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().min(2).max(40),
|
||||
});
|
||||
|
||||
export const MemberInviteForm = ({ orgId, userId }: { orgId: number, userId: string }) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateInvite = async (values: { email: string }) => {
|
||||
const res = await createInvite(values.email, userId, orgId);
|
||||
if (isServiceError(res)) {
|
||||
toast({
|
||||
description: `❌ Failed to create invite`
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Invite created successfully!`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleCreateInvite)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className="mt-5" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
packages/web/src/app/settings/components/memberTable.tsx
Normal file
38
packages/web/src/app/settings/components/memberTable.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use client';
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { User } from "@sourcebot/db";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { MemberColumnInfo, memberTableColumns } from "./memberTableColumns";
|
||||
|
||||
export interface MemberInfo {
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface MemberTableProps {
|
||||
initialMembers: MemberInfo[];
|
||||
}
|
||||
|
||||
export const MemberTable = ({ initialMembers }: MemberTableProps) => {
|
||||
const [members, setMembers] = useState<MemberInfo[]>(initialMembers);
|
||||
|
||||
const memberRows: MemberColumnInfo[] = useMemo(() => {
|
||||
return members.map(member => {
|
||||
return {
|
||||
name: member.name!,
|
||||
role: member.role!,
|
||||
}
|
||||
})
|
||||
}, [members]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTable
|
||||
columns={memberTableColumns()}
|
||||
data={memberRows}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
'use client'
|
||||
|
||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||
|
||||
export type MemberColumnInfo = {
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export const memberTableColumns = (): ColumnDef<MemberColumnInfo>[] => {
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => {
|
||||
const member = row.original;
|
||||
return <div>{member.name}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
cell: ({ row }) => {
|
||||
const member = row.original;
|
||||
return <div>{member.role}</div>;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
17
packages/web/src/app/settings/layout.tsx
Normal file
17
packages/web/src/app/settings/layout.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { NavigationMenu } from "../components/navigationMenu";
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<NavigationMenu />
|
||||
<main className="flex-grow flex justify-center p-4 bg-[#fafafa] dark:bg-background relative">
|
||||
<div className="w-full max-w-6xl rounded-lg p-6">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
80
packages/web/src/app/settings/page.tsx
Normal file
80
packages/web/src/app/settings/page.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Header } from "../components/header";
|
||||
import { auth } from "@/auth";
|
||||
import { getUser } from "@/data/user";
|
||||
import { prisma } from "@/prisma";
|
||||
import { MemberTable } from "./components/memberTable";
|
||||
import { MemberInviteForm } from "./components/memberInviteForm";
|
||||
import { InviteTable } from "./components/inviteTable";
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const fetchData = async () => {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user || !user.activeOrgId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const members = await prisma.user.findMany({
|
||||
where: {
|
||||
orgs: {
|
||||
some: {
|
||||
orgId: user.activeOrgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
orgs: {
|
||||
where: {
|
||||
orgId: user.activeOrgId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const invites = await prisma.invite.findMany({
|
||||
where: {
|
||||
orgId: user.activeOrgId,
|
||||
},
|
||||
});
|
||||
|
||||
const memberInfo = members.map((member) => ({
|
||||
name: member.name!,
|
||||
role: member.orgs[0].role,
|
||||
}));
|
||||
|
||||
const inviteInfo = invites.map((invite) => ({
|
||||
id: invite.id,
|
||||
email: invite.recipientEmail,
|
||||
createdAt: invite.createdAt,
|
||||
}));
|
||||
|
||||
return { user, memberInfo, inviteInfo };
|
||||
};
|
||||
|
||||
const data = await fetchData();
|
||||
if (!data) {
|
||||
return <div>Error: Unable to fetch data</div>;
|
||||
}
|
||||
const { user, memberInfo, inviteInfo } = data;
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header>
|
||||
<h1 className="text-3xl">Settings</h1>
|
||||
</Header>
|
||||
<div>
|
||||
<MemberInviteForm orgId={user.activeOrgId!} userId={user.id} />
|
||||
<InviteTable initialInvites={inviteInfo} />
|
||||
<MemberTable initialMembers={memberInfo} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import 'next-auth/jwt';
|
||||
import NextAuth, { User as AuthJsUser, DefaultSession } from "next-auth"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
import Google from "next-auth/providers/google"
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||
import { prisma } from "@/prisma";
|
||||
import type { Provider } from "next-auth/providers"
|
||||
import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_SECRET } from "./lib/environment";
|
||||
import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, AUTH_SECRET } from "./lib/environment";
|
||||
import { User } from '@sourcebot/db';
|
||||
import { notAuthenticated, notFound, unexpectedError } from "@/lib/serviceError";
|
||||
import { getUser } from "./data/user";
|
||||
|
|
@ -28,6 +29,10 @@ const providers: Provider[] = [
|
|||
clientId: AUTH_GITHUB_CLIENT_ID,
|
||||
clientSecret: AUTH_GITHUB_CLIENT_SECRET,
|
||||
}),
|
||||
Google({
|
||||
clientId: AUTH_GOOGLE_CLIENT_ID!,
|
||||
clientSecret: AUTH_GOOGLE_CLIENT_SECRET!,
|
||||
})
|
||||
];
|
||||
|
||||
// @see: https://authjs.dev/guides/pages/signin
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ export const NODE_ENV = process.env.NODE_ENV;
|
|||
export const AUTH_SECRET = getEnv(process.env.AUTH_SECRET); // Generate using `npx auth secret`
|
||||
export const AUTH_GITHUB_CLIENT_ID = getEnv(process.env.AUTH_GITHUB_CLIENT_ID);
|
||||
export const AUTH_GITHUB_CLIENT_SECRET = getEnv(process.env.AUTH_GITHUB_CLIENT_SECRET);
|
||||
export const AUTH_GOOGLE_CLIENT_ID = getEnv(process.env.AUTH_GOOGLE_CLIENT_ID);
|
||||
export const AUTH_GOOGLE_CLIENT_SECRET = getEnv(process.env.AUTH_GOOGLE_CLIENT_SECRET);
|
||||
|
|
@ -23,6 +23,12 @@ const apiMiddleware = (req: NextAuthRequest) => {
|
|||
}
|
||||
|
||||
const defaultMiddleware = (req: NextAuthRequest) => {
|
||||
// if we're trying to redeem an invite while not authed we continue to the redeem page so
|
||||
// that we can pipe the invite_id to the login page
|
||||
if (!req.auth && req.nextUrl.pathname === "/redeem") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
if (!req.auth && req.nextUrl.pathname !== "/login") {
|
||||
const newUrl = new URL("/login", req.nextUrl.origin);
|
||||
return NextResponse.redirect(newUrl);
|
||||
|
|
|
|||
Loading…
Reference in a new issue