Skip to content

Commit

Permalink
update(web): Add support for AniList OAuth2 login
Browse files Browse the repository at this point in the history
  • Loading branch information
Nekidev committed Jan 26, 2024
1 parent 181fb87 commit 33d7833
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 147 deletions.
4 changes: 4 additions & 0 deletions src/web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# AniList's OAuth2
PROVIDER_ANILIST_CLIENT_ID=
PROVIDER_ANILIST_CLIENT_SECRET=
PROVIDER_ANILIST_REDIRECT_URI=
9 changes: 7 additions & 2 deletions src/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"next": "14.1.0"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20.11.6",
"@types/react": "18.2.48",
"typescript": "5.3.3"
}
}
47 changes: 47 additions & 0 deletions src/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/web/src/app/api/auth/oauth2/anilist/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as crypto from "crypto";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { type NextRequest } from "next/server";

export const dynamic = "force-dynamic"; // defaults to auto

function encrypt(string: string, key: string, iv: string): string {
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
let encryptedString = cipher.update(string, "utf-8", "hex");
encryptedString += cipher.final("hex");
return encryptedString;
}

export async function GET(request: NextRequest) {
const res = await fetch("https://anilist.co/api/v2/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
grant_type: "authorization_code",
client_id: process.env.PROVIDER_ANILIST_CLIENT_ID,
client_secret: process.env.PROVIDER_ANILIST_CLIENT_SECRET,
redirect_uri: process.env.PROVIDER_ANILIST_REDIRECT_URI,
code: request.nextUrl.searchParams.get("code"),
}),
});

if (!res.ok) {
redirect(`/code?status=fail`);
}

const json = await res.json();

const cookieStore = cookies();

redirect(
`/code?status=success&key=${encrypt(
json.access_token,
cookieStore.get("key")?.value!,
cookieStore.get("iv")?.value!
)}`
);
}
31 changes: 31 additions & 0 deletions src/web/src/app/api/auth/oauth2/anilist/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as crypto from "crypto";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { type NextRequest } from "next/server";

export const dynamic = "force-dynamic"; // defaults to auto

export async function GET(request: NextRequest) {
const key = request.nextUrl.searchParams.get("key");
const iv = request.nextUrl.searchParams.get("iv");

if (!key || !iv) {
redirect("/code?status=fail");
}

const cookieStore = cookies();
cookieStore.set("key", key, {
expires: new Date(new Date().getTime() + 5 * 60 * 1000),
});
cookieStore.set("iv", iv, {
expires: new Date(new Date().getTime() + 5 * 60 * 1000),
});

redirect(
`https://anilist.co/api/v2/oauth/authorize?client_id=${encodeURIComponent(
process.env.PROVIDER_ANILIST_CLIENT_ID
)}&redirect_uri=${encodeURIComponent(
process.env.PROVIDER_ANILIST_REDIRECT_URI
)}&response_type=code`
);
}
66 changes: 66 additions & 0 deletions src/web/src/app/code/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useSearchParams } from "next/navigation";

export default function Code() {
const searchParams = useSearchParams();

if (searchParams.get("status") !== "success") {
return (
<>
<h1>Something went wrong</h1>
<p>
For some reason you couldn't be authenticated. You can close
this tab.
</p>
<p>
<i>
If the issue persists, ask for help in our Discord
server. You can find the link <a href="/">here</a>.
</i>
</p>
</>
);
}
return (
<>
<h1>Success!</h1>
<p>Go back to Naoka and paste the following code:</p>
<div
style={{
display: "flex",
flexDirection: "row",
width: "100%",
gap: "0.5rem",
alignItems: "center",
}}
>
<input
type="text"
disabled
style={{
flex: 1,
userSelect: "all",
MozUserSelect: "all",
WebkitUserSelect: "all"
}}
value={searchParams.get("code")}
/>
<button
onClick={(e) => {
navigator.clipboard.writeText(searchParams.get("code"));
e.target.innerText = "Copied!";
setTimeout(() => {
e.target.innerText = "Copy to clipboard";
}, 3000);
}}
>
Copy to clipboard
</button>
</div>
<p>
<i>You can close this tab after pasting the code in Naoka.</i>
</p>
</>
);
}
22 changes: 21 additions & 1 deletion src/web/src/app/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@ export default function RootLayout({ children }) {
justifyContent: "center",
}}
>
<div id="top"></div>
<div
style={{
width: "100%",
maxWidth: "30em",
paddingBottom: "2rem"
paddingBottom: "2rem",
}}
>
{children}
<hr />
<p>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
}}
>
<span>
<a href="#top">Go to the top</a> &middot;{" "}
<a href="/">Home</a>{" "}
&middot; <a href="https://nyeki.dev">Nyeki</a>
</span>
<span>MIT License</span>
</div>
</p>
</div>
</body>
</html>
Expand Down
Loading

0 comments on commit 33d7833

Please sign in to comment.