Skip to content

Commit

Permalink
feat: add authentication!
Browse files Browse the repository at this point in the history
chore: add new environment variables to .env.dist file
fix(route.ts): add authentication check to GET method
feat(route.ts): add authentication middleware to auth route
refactor(layout.tsx): change type of children prop to React.ReactNode
feat(page.tsx): add authentication check to HomePage component
feat(auth.ts): add authentication providers and callbacks
feat(SidebarDesktop.tsx): add LogoutButton component to user profile section
feat(HomeButton.tsx): add LoginButton and LogoutButton components
feat(middleware.ts): add middleware to protect all routes except for API and static files
feat(next.config.js): add external packages to serverComponentsExternalPackages option in experimental config

feat(package.json): update next-auth and octokit dependencies to latest versions to improve security and functionality
chore(package.json): bump version to 1.2.0 to reflect changes made in this commit
  • Loading branch information
masterkain committed May 20, 2023
1 parent c612b08 commit 64e76bf
Show file tree
Hide file tree
Showing 12 changed files with 784 additions and 12 deletions.
34 changes: 34 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@
# DATABASE_URL="postgresql://airbroke:airbroke@localhost:6432/airbroke-development?schema=public&pgbouncer=true"
# DIRECT_URL="postgresql://airbroke:airbroke@localhost:5432/airbroke-development?schema=public"

# Endpoint protection
CORS_ORIGINS="http://localhost:3000,https://my.browserapp.com"

# AI toolbox
OPENAI_API_KEY="sk-xxx"
OPENAI_ORGANIZATION=""

# authentication
NEXTAUTH_SECRET="" # A random string used to hash tokens, sign cookies and generate cryptographic keys.
NEXTAUTH_URL="http://localhost:3000" # The URL of your application, used for signing cookies and OAuth secrets, defaults to http://localhost:3000
NEXTAUTH_DEBUG="false"

GITHUB_ID=""
GITHUB_SECRET=""
GITHUB_ORGS="" # optional, if you want to restrict access to specific organization(s), comma separated

ATLASSIAN_ID=""
ATLASSIAN_SECRET=""

GOOGLE_ID=""
GOOGLE_SECRET=""
GOOGLE_DOMAINS="" # optional, if you want to restrict access to specific domain(s), comma separated

COGNITO_ID=""
COGNITO_SECRET=""
COGNITO_ISSUER="" # a URL, that looks like this: https://cognito-idp.{region}.amazonaws.com/{PoolId}

GITLAB_ID=""
GITLAB_SECRET=""

KEYCLOAK_ID=""
KEYCLOAK_SECRET=""
KEYCLOAK_ISSUER="" # issuer should include the realm e.g. https://my-keycloak-domain.com/realms/My_Realm

AZURE_AD_CLIENT_ID=""
AZURE_AD_CLIENT_SECRET=""
AZURE_AD_TENANT_ID=""
10 changes: 10 additions & 0 deletions app/api/ai/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/db';
import { ChatGPTAPI } from 'chatgpt';
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) {
return new NextResponse(
JSON.stringify({ status: "fail", message: "You are not logged in" }),
{ status: 401 }
);
}

// const pass = request.nextUrl.searchParams.get('pass');
const occurrenceId = request.nextUrl.searchParams.get('occurrence');

Expand Down
6 changes: 6 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
3 changes: 1 addition & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Inter } from 'next/font/google';
import { ReactNode } from 'react';
import './globals.css';

export const metadata = {
Expand All @@ -13,7 +12,7 @@ const inter = Inter({
variable: '--font-inter',
});

export default function RootLayout({ children }: { children: ReactNode }) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="h-full bg-airbroke-900">
<body className={`h-full antialiased scrollbar-none ${inter.className}`}>{children}</body>
Expand Down
12 changes: 8 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import Background from '@/components/Background';
import { authOptions } from '@/lib/auth';
import screenshot from '@/public/screenshot.png';
import { getServerSession } from 'next-auth';
import Image from 'next/image';
import Link from 'next/link';
import { FaGithub } from 'react-icons/fa';
import { MdBrokenImage } from 'react-icons/md';

export default function HomePage() {
export default async function HomePage() {
const currentYear = new Date().getFullYear();
const session = await getServerSession(authOptions);

return (
<div className="h-full bg-gray-900">
Expand All @@ -19,7 +22,7 @@ export default function HomePage() {
<div className="mt-24 sm:mt-32 lg:mt-16">
<Link href="https://github.com/icoretech/airbroke/releases" className="inline-flex space-x-6">
<span className="rounded-full bg-indigo-500/10 px-3 py-1 text-sm font-semibold leading-6 text-indigo-400 ring-1 ring-inset ring-indigo-500/20">
Whats new
Latest Releases
</span>
</Link>
</div>
Expand All @@ -30,12 +33,13 @@ export default function HomePage() {
Self-hosted, Cost-effective, Open Source Error Tracking for a Sustainable Startup Journey.
</p>
<div className="mt-10 flex items-center gap-x-6">
<a
<Link
href="/projects"
className="rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-400"
>
Go to Projects
</a>
</Link>

<Link href="https://github.com/icoretech/airbroke" className="text-sm font-semibold leading-6 text-white">
Learn more <span aria-hidden="true"></span>
</Link>
Expand Down
25 changes: 25 additions & 0 deletions components/HomeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import { signIn, signOut } from 'next-auth/react';

export function LoginButton() {
return (
<button
onClick={() => signIn()}
className="rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-400"
>
Sign in
</button>
);
}

export function LogoutButton() {
return (
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-400"
>
Sign Out
</button>
);
}
11 changes: 10 additions & 1 deletion components/SidebarDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from 'next/link';
import { MdBrokenImage } from 'react-icons/md';
import { SlPlus } from 'react-icons/sl';
import { TbBrandGithub } from 'react-icons/tb';
import { LogoutButton } from './HomeButton';

function groupBy<T>(array: T[], key: keyof T) {
return array.reduce((result: { [key: string]: T[] }, item) => {
Expand Down Expand Up @@ -64,7 +65,7 @@ export default async function SidebarDesktop({ selectedProject }: { selectedProj
</ul>
</li>
))}
<li className="mt-auto">
<li>
<Link
href="/projects/new"
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 shadow-sm transition-colors duration-200 hover:bg-indigo-500 hover:text-white"
Expand All @@ -75,6 +76,14 @@ export default async function SidebarDesktop({ selectedProject }: { selectedProj
</li>
</ul>
</nav>

{/* User Profile Section */}
<div className="flex flex-col items-center gap-y-5 px-4 py-4">
{/* User info can go here, e.g. avatar and username */}

{/* Logout Button */}
<LogoutButton />
</div>
</div>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { NextAuthOptions, Profile } from "next-auth";
import AtlassianProvider from "next-auth/providers/atlassian";
import AzureADProvider from "next-auth/providers/azure-ad";
import CognitoProvider from "next-auth/providers/cognito";
import GithubProvider from "next-auth/providers/github";
import GitlabProvider from "next-auth/providers/gitlab";
import GoogleProvider from "next-auth/providers/google";
import KeycloakProvider from "next-auth/providers/keycloak";
import { Octokit } from "octokit";

type ExtendedProfile = Profile & { [key: string]: any; };

const getProviders = () => {
let providers = [];

if (process.env.GITHUB_ID && process.env.GITHUB_SECRET) {
providers.push(GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
authorization: {
url: "https://github.com/login/oauth/authorize",
params: { scope: "read:user user:email user read:org" },
},
}));
}

if (process.env.ATLASSIAN_ID && process.env.ATLASSIAN_SECRET) {
providers.push(AtlassianProvider({
clientId: process.env.ATLASSIAN_ID,
clientSecret: process.env.ATLASSIAN_SECRET,
}));
}

if (process.env.GOOGLE_ID && process.env.GOOGLE_SECRET) {
providers.push(GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}));
}

if (process.env.COGNITO_ID && process.env.COGNITO_SECRET) {
providers.push(CognitoProvider({
clientId: process.env.COGNITO_ID,
clientSecret: process.env.COGNITO_SECRET,
issuer: process.env.COGNITO_ISSUER,
}));
}

if (process.env.GITLAB_ID && process.env.GITLAB_SECRET) {
providers.push(GitlabProvider({
clientId: process.env.GITLAB_ID,
clientSecret: process.env.GITLAB_SECRET,
}));
}

if (process.env.KEYCLOAK_ID && process.env.KEYCLOAK_SECRET) {
providers.push(KeycloakProvider({
clientId: process.env.KEYCLOAK_ID,
clientSecret: process.env.KEYCLOAK_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
}));
}

if (process.env.AZURE_AD_CLIENT_ID && process.env.AZURE_AD_CLIENT_SECRET) {
providers.push(AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
}));
}

return providers;
};

export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
debug: process.env.NEXTAUTH_DEBUG === "true",
providers: getProviders(),
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
const extendedProfile = profile as ExtendedProfile;

if (account?.provider === "google" && process.env.GOOGLE_DOMAINS) {
const domains = process.env.GOOGLE_DOMAINS.split(",");
const emailDomain = extendedProfile?.email?.split("@")[1];
return extendedProfile?.email_verified && emailDomain && domains.includes(emailDomain);
}

if (account?.provider === "github" && process.env.GITHUB_ORGS) {
const allowedOrgs = process.env.GITHUB_ORGS.split(",");
const token = account.access_token;
const octokit = new Octokit({ auth: token, userAgent: "airbroke" });
const orgsResponse = await octokit.rest.orgs.listForAuthenticatedUser();
const userOrgs = orgsResponse.data.map(org => org.login);

// Check if the user is part of at least one of the allowed organizations
return userOrgs.some(org => allowedOrgs.includes(org));
}
return true;
},
},
theme: {
colorScheme: "dark", // "auto" | "dark" | "light"
brandColor: "#192231", // Hex color code
logo: "", // Absolute URL to image
buttonText: "" // Hex color code
}
};
15 changes: 15 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export { default } from "next-auth/middleware";

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - the homepage ("/")
*/
'/((?!api|_next/static|_next/image|favicon.ico).+)',
],
};
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const nextConfig = {
output: 'standalone',
experimental: {
serverActions: true,
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs', 'chatgpt', '@octokit'],
},
async rewrites() {
return [
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "airbroke",
"version": "1.1.1",
"version": "1.2.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -23,7 +23,9 @@
"chatgpt": "file:vendor/chatgpt-5.2.4.tgz",
"nanoid": "4.0.2",
"next": "13.4.3",
"next-auth": "^4.22.1",
"numeral": "^2.0.6",
"octokit": "^2.0.16",
"postcss": "8.4.23",
"prettier": "2.8.8",
"prettier-plugin-tailwindcss": "0.3.0",
Expand Down
Loading

0 comments on commit 64e76bf

Please sign in to comment.