Skip to content
Closed
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
4 changes: 4 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Github
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URL=
32 changes: 32 additions & 0 deletions src/app/api/oauth/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { redirect } from "next/navigation";
import { UrlBuilder } from "@/core/url/url";
import { GithubUrlBuilder } from "@/features/auth/github-url";

const GITHUB_SCOPE = "read:user";
const GITHUB_BASE_URL = "https://github.com/";
const GITHUB_AUTHORIZE_PATH = "login/oauth/authorize";
const GITHUB_REDIRECT_URL = "http://localhost:3000/api/oauth/callback";
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;

export async function GET() {
if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) return Response.error();

const githubUrl = new GithubUrlBuilder();

githubUrl.addPath(GITHUB_AUTHORIZE_PATH);
githubUrl.addQueryParam("client_id", encodeURI(GITHUB_CLIENT_ID));
githubUrl.addQueryParam("redirect_uri", encodeURI(GITHUB_REDIRECT_URL));
githubUrl.addQueryParam("scope", encodeURI(GITHUB_SCOPE));

const redirectUrl = githubUrl.getUrl();

if (redirectUrl) {
redirect(redirectUrl);
}

return Response.json(
{ message: "Redirect URL not available" },
{ status: 500 },
);
}
62 changes: 62 additions & 0 deletions src/app/api/oauth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
GITHUB_ACCESS_TOKEN_PATH,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
} from "@/features/auth/constants";
import { GithubUrlBuilder } from "@/features/auth/github-url";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET)
return NextResponse.json(
{ message: "Missing client ID or secret" },
{ status: 500 },
);

const searchParams = request.url.split("?");
if (searchParams.length <= 0)
return NextResponse.json(
{ message: "Missing search params" },
{ status: 500 },
);

const code = new URLSearchParams(searchParams[1]).get("code");
if (!code)
return NextResponse.json({ message: "Missing code" }, { status: 500 });

const accessTokenUrl = new GithubUrlBuilder();
accessTokenUrl.addPath(GITHUB_ACCESS_TOKEN_PATH);
accessTokenUrl.addQueryParam("code", code);
accessTokenUrl.addQueryParam("client_id", GITHUB_CLIENT_ID);
accessTokenUrl.addQueryParam("client_secret", GITHUB_CLIENT_SECRET);
const tokenUrlUrl = accessTokenUrl.getUrl();
if (!tokenUrlUrl)
return NextResponse.json({ message: "Missing token URL" }, { status: 500 });

const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("Content-Type", "application/json");
const githubRequest = await fetch(tokenUrlUrl, {
method: "POST",
headers,
});

if (!githubRequest.ok)
return NextResponse.json(
{ message: "Failed to fetch token" },
{ status: 500 },
);

const data = await githubRequest.json();
const destinationUrl = new URL("/", new URL(request.url).origin);
const response = NextResponse.redirect(destinationUrl, { status: 302 });

response.cookies.set("github_access_token", data.access_token, {
path: "/",
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
secure: true,
});

return response;
}
43 changes: 43 additions & 0 deletions src/core/url/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface Url {
url?: URL;
setBaseUrl(baseUrl: string): void;
addPath(path: string): void;
addQueryParam(name: string, value: string): void;
getUrl(): string | null;
reset(): void;
}

export class UrlBuilder implements Url {
url?: URL;

constructor(baseUrl: string) {
this.setBaseUrl(`${baseUrl}?`);
}

setBaseUrl(baseUrl: string) {
this.url = new URL(baseUrl);
}

addPath(path: string) {
if (this.url) {
this.url.pathname = this.url.pathname.concat(path);
}
}

addQueryParam(name: string, value: string) {
this.url?.searchParams.set(name, value);
}

getUrl() {
if (!this.url || !this.url.toString) return null;

const url = this.url.toString();
this.reset();

return url;
}

reset() {
this.url = undefined;
}
}
17 changes: 17 additions & 0 deletions src/features/auth/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const GITHUB_ACCESS_TOKEN_PATH = "login/oauth/access_token";
const GITHUB_AUTHORIZE_PATH = "login/oauth/authorize";
const GITHUB_BASE_URL = "https://github.com/";
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const GITHUB_REDIRECT_URL = process.env.GITHUB_REDIRECT_URL;
const GITHUB_SCOPE = "read:user";

export {
GITHUB_ACCESS_TOKEN_PATH,
GITHUB_AUTHORIZE_PATH,
GITHUB_BASE_URL,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
GITHUB_REDIRECT_URL,
GITHUB_SCOPE,
};
8 changes: 8 additions & 0 deletions src/features/auth/github-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { UrlBuilder } from "@/core/url/url";
import { GITHUB_BASE_URL } from "./constants";

export class GithubUrlBuilder extends UrlBuilder {
constructor() {
super(GITHUB_BASE_URL);
}
}