Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
}
684 changes: 484 additions & 200 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
},
"yourPosition": "Your position",
"error": {
"notFound": "Leaderboard not found",
"loadingAllLeaderboards": "An error occurred while loading your leaderboard list, try again later. If the error persists, please contact us."
}
},
Expand Down
1 change: 1 addition & 0 deletions public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
},
"yourPosition": "Sija",
"error": {
"notFound": "Tulostaulua ei löytynyt.",
"loadingAllLeaderboards": "Tulostaulujesi lataamisessa tapahtui virhe, yritä uudelleen myöhemmin. Jos virhe jatkuu, ole hyvä ja ota yhteyttä meihin."
}
},
Expand Down
121 changes: 121 additions & 0 deletions src/api/baseApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"use server";

import { cookies, headers } from "next/headers";
import { GetRequestError, PostRequestError } from "../types";

export const getRequest = async <T>(path: string) => {
const token = cookies().get("token")?.value;
if (!token) {
return {
error: GetRequestError.Unauthorized as const,
};
}

const ip = headers().get("client-ip") ?? "Unknown IP";

const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, {
headers: {
Authorization: `Bearer ${token}`,
"client-ip": ip,
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
cache: "no-cache",
});

if (!response.ok) {
if (response.status === 401) {
return {
error: GetRequestError.Unauthorized as const,
response,
};
}

if (response.status === 429) {
return {
error: GetRequestError.RateLimited as const,
response,
};
}

return {
error: GetRequestError.UnknownError as const,
path,
response,
};
}

const data = (await response.json()) as T;

return data;
};

type WithResponseBody<R> =
| { data: R }
| { error: PostRequestError; statusCode: number };

type NoResponseBody = WithResponseBody<null>;

async function baseFetch(
path: string,
body?: unknown,
method: string = "POST",
) {
const tokenCookieName = "token";
const token = cookies().get(tokenCookieName)?.value;

const h = new Headers({
"Content-Type": "application/json",
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
});

if (token) {
h.set("Authorization", `Bearer ${token}`);
}

const response = await fetch(process.env.NEXT_PUBLIC_API_URL + path, {
method,
headers: h,
cache: "no-cache",
body: body ? JSON.stringify(body) : undefined,
});

if (!response.ok) {
if (response.status === 429) {
return {
error: PostRequestError.RateLimited,
statusCode: response.status,
};
}

return {
error: PostRequestError.UnknownError,
statusCode: response.status,
};
}

return response;
}

export async function postRequestWithResponse<R>(
path: string,
body?: unknown,
method: string = "POST",
): Promise<WithResponseBody<R>> {
const response = await baseFetch(path, body, method);
if ("error" in response) return response;

const data = (await response.json()) as R;
return { data };
}

export async function postRequestWithoutResponse(
path: string,
body?: unknown,
method: string = "POST",
): Promise<NoResponseBody> {
const response = await baseFetch(path, body, method);
if ("error" in response) return response;

return { data: null };
}
47 changes: 3 additions & 44 deletions src/api/friendsApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CurrentActivityApiResponse } from "../types";
import { cookies, headers } from "next/headers";
import { getRequest } from "./baseApi";

export interface ApiFriendsResponseItem {
username: string;
Expand All @@ -11,46 +11,5 @@ export interface ApiFriendsResponseItem {
status: CurrentActivityApiResponse | null;
}

export const getFriendsList = async () => {
const token = cookies().get("token")?.value;
if (!token) {
return {
error: "Unauthorized" as const,
};
}

const friendsPromise = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/friends/list`,
{
headers: {
Authorization: `Bearer ${token}`,
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
cache: "no-cache",
},
);

if (!friendsPromise.ok) {
if (friendsPromise.status === 401) {
return {
error: "Unauthorized" as const,
};
}

if (friendsPromise.status === 429) {
return {
error: "Too many requests" as const,
};
}

return {
error: "Unknown error when fetching /friends/list" as const,
status: friendsPromise.status,
};
}

const data = (await friendsPromise.json()) as ApiFriendsResponseItem[];

return data;
};
export const getFriendsList = () =>
getRequest<ApiFriendsResponseItem[]>("/friends/list");
110 changes: 15 additions & 95 deletions src/api/leaderboardApi.ts
Original file line number Diff line number Diff line change
@@ -1,123 +1,43 @@
"use server";

import { cookies, headers } from "next/headers";
import {
CreateLeaderboardError,
GetLeaderboardError,
GetLeaderboardsError,
Leaderboard,
LeaderboardData,
} from "../types";
import { getRequest, postRequestWithResponse } from "./baseApi";

export const getMyLeaderboards = async () => {
const token = cookies().get("token")?.value;
if (!token) {
return { error: GetLeaderboardsError.Unauthorized };
}

try {
const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/users/@me/leaderboards",
{
headers: {
Authorization: `Bearer ${token}`,
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
cache: "no-cache",
},
);

if (!response.ok) {
if (response.status === 401) {
return { error: GetLeaderboardsError.Unauthorized };
} else if (response.status === 429) {
return { error: GetLeaderboardsError.RateLimited };
}

const errorText = await response.text();
console.log("Unknown error when getting user's leaderboards:", errorText);

return { error: GetLeaderboardsError.UnknownError };
}

const data = (await response.json()) as Leaderboard[];

return data;
} catch (e: unknown) {
console.error("Unknown error when getting user's leaderboards:", e);
return { error: GetLeaderboardsError.UnknownError };
}
};
export const getMyLeaderboards = () =>
getRequest<Leaderboard[]>("/users/@me/leaderboards");

export const getLeaderboard = async (leaderboardName: string) => {
const token = cookies().get("token")?.value;
if (!token) {
return { error: GetLeaderboardError.Unauthorized };
}

const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + `/leaderboards/${leaderboardName}`,
{
headers: {
Authorization: `Bearer ${token}`,
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
cache: "no-cache",
next: {
tags: [`leaderboard-${leaderboardName}`],
},
},
const data = await getRequest<LeaderboardData>(
`/leaderboards/${leaderboardName}`,
);

if (!response.ok) {
if (response.status === 401) {
return { error: GetLeaderboardError.Unauthorized };
} else if (response.status === 429) {
return { error: GetLeaderboardError.RateLimited };
if ("error" in data) {
if (data.response?.status === 404) {
return { error: GetLeaderboardError.LeaderboardNotFound };
}

const errorText = await response.text();
console.log(errorText);

return { error: GetLeaderboardError.UnknownError };
}

const data = (await response.json()) as LeaderboardData;

return data;
};

export const createLeaderboard = async (leaderboardName: string) => {
const token = cookies().get("token")?.value;
const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/leaderboards/create",
const data = await postRequestWithResponse<{ invite_code: string }>(
"/leaderboards/create",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
cache: "no-cache",
body: JSON.stringify({ name: leaderboardName }),
name: leaderboardName,
},
);

if (!response.ok) {
if (response.status === 409) {
return { error: CreateLeaderboardError.AlreadyExists };
} else if (response.status === 429) {
return { error: CreateLeaderboardError.RateLimited };
}

console.error("Error while creating leaderboard:", await response.text());
return { error: CreateLeaderboardError.UnknownError };
if ("error" in data && data.statusCode === 409) {
return {
error: CreateLeaderboardError.AlreadyExists,
};
}

const data = (await response.json()) as { invite_code: string };

return data;
};
Loading