Skip to content
Open
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
2 changes: 1 addition & 1 deletion package-lock.json

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

31 changes: 23 additions & 8 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,18 @@
"create": "Create",
"createNewLeaderboard": "Create new leaderboard",
"deleteLeaderboard": "Delete leaderboard",
"deletionNotification": {
"successTitle": "Leaderboard deleted",
"successDescription": "Leaderboard was successfully deleted"
},
"demote": "Demote",
"inviteCode": "Invite code",
"join": {
"alreadyMember": "You are already a member of this leaderboard",
"genericError": "Error joining leaderboard",
"join": "Join",
"leaderboardCode": "Leaderboard code",
"leaderboardCodeInvalid": "Leaderboard code must start with \"ttlic_\", and be followed by 24 alphanumeric characters.",
"leaderboardCodeInvalid": "Leaderboard code must start with \"ttlic_\", and be followed by 32 alphanumeric characters.",
"leaderboardCodeRequired": "Leaderboard code is required",
"notFound": "Leaderboard not found"
},
Expand All @@ -118,6 +122,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 Expand Up @@ -161,12 +166,22 @@
"makePublic": "Make my account public",
"title": "Account visibility"
},
"sudoOperation": {
"title": "Confirm password",
"passwordLabel": "Password",
"wrongPassword": "The provided password is incorrect",
"passwordRequired": "Password is required",
"confirmButton": "Confirm"
},
"authenticationToken": {
"title": "Authentication token",
"tooltip": {
"install": "Get your extension from here!",
"label": "This token is used for authentication in your code editor."
}
},
"passwordRequiredDescription": "Please provide your password to regenerate the authentication token",
"regenerateSuccessTitle": "Authentication token regenerated",
"regenerateSuccessDescription": "Your authentication token was successfully regenerated, please remember to update the new token to your code editors."
},
"changePassword": {
"confirm": {
Expand Down Expand Up @@ -213,13 +228,13 @@
}
},
"deleteAccount": {
"title": "Account deletion",
"button": "Delete account",
"modal": {
"button": "Delete",
"text": "Type your password to confirm the deletion. This action can not be undone.",
"title": "Delete account"
},
"title": "Delete account"
"modalDescription": "Type your password to confirm the deletion. This action can not be undone.",
"notification": {
"successTitle": "Account deleted",
"successDescription": "Your account and all data has been successfully deleted"
}
},
"friendCode": {
"title": "Friend code",
Expand Down
31 changes: 23 additions & 8 deletions public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,18 @@
"create": "Luo",
"createNewLeaderboard": "Luo uusi tulostaulu",
"deleteLeaderboard": "Poista tulostaulu",
"deletionNotification": {
"successTitle": "Tulostaulu poistettu",
"successDescription": "Tulostaulu on poistettu onnistuneesti"
},
"demote": "Alenna",
"inviteCode": "Kutsukoodi",
"join": {
"alreadyMember": "Olet jo tämän tulostaulun jäsen.",
"genericError": "Tulostauluun liittyminen epäonnistui.",
"join": "Liity",
"leaderboardCode": "Tulostaulukoodi",
"leaderboardCodeInvalid": "Kutsukoodin tulee alkaa \"ttlic_\", ja sen jälkeen täytyy olla 24 alphanumeerista kirjainta.",
"leaderboardCodeInvalid": "Kutsukoodin tulee alkaa \"ttlic_\", ja sen jälkeen täytyy olla 32 alphanumeerista kirjainta.",
"leaderboardCodeRequired": "Kutsukoodi vaaditaan.",
"notFound": "Tulostaulua ei löytynyt."
},
Expand All @@ -118,6 +122,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 Expand Up @@ -161,12 +166,22 @@
"makePublic": "Muuta julkiseksi",
"title": "Tilin näkyvyys"
},
"sudoOperation": {
"title": "Vahvista salasana",
"passwordLabel": "Salasana",
"wrongPassword": "Antamasi salasana on väärin",
"passwordRequired": "Salasana vaaditaan",
"confirmButton": "Vahvista"
},
"authenticationToken": {
"title": "Tunnistautumistunnus",
"tooltip": {
"install": "Asenna Testaustime-laajennus täältä!",
"label": "Tätä tunnusta käytetään tunnistautumiseen koodieditorissasi."
}
},
"passwordRequiredDescription": "Vahvista salasanasi generoidaksesi tunnistautumistunnus uudelleen",
"regenerateSuccessTitle": "Tunnistautumistunnus generoitu uudelleen",
"regenerateSuccessDescription": "Tunnistautumistunnus generoitiin uudelleen, muista päivittää uusi tunnus myös editoreihisi."
},
"changePassword": {
"confirm": {
Expand Down Expand Up @@ -213,13 +228,13 @@
}
},
"deleteAccount": {
"title": "Tilin poisto",
"button": "Poista tili",
"modal": {
"button": "Poista",
"text": "Kirjoita salasanasi vahvistaaksesi tilisi poistaminen. Tätä toimintoa ei voi peruuttaa.",
"title": "Käyttäjän poistaminen"
},
"title": "Poista tili"
"modalDescription": "Kirjoita salasanasi vahvistaaksesi tilisi poistaminen. Tätä toimintoa ei voi peruuttaa.",
"notification": {
"successTitle": "Tili poistettu",
"successDescription": "Tilisi ja kaikki sen tiedot on poistettu onnistuneesti"
}
},
"friendCode": {
"title": "Kaverikoodi",
Expand Down
132 changes: 132 additions & 0 deletions src/api/baseApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use server";

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

export const getRequest = async <T>(path: string) => {
if (process.env.NEXT_PUBLIC_API_URL == null)
throw new Error("API URL was not defined");

const token = cookies().get("token")?.value;

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

const h = new Headers({
"client-ip": ip,
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
});

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

const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, {
headers: h,
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",
) {
if (process.env.NEXT_PUBLIC_API_URL == null)
throw new Error("API URL was not defined");

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,
};
} else if (response.status === 401) {
return {
error: PostRequestError.Unauthorized,
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");
Loading