Rework authentication

This commit is contained in:
Aslan 2026-01-01 17:06:31 +01:00
parent a85330e8cf
commit c07d33bcc9
17 changed files with 317 additions and 128 deletions

1
env
View file

@ -1,2 +1,3 @@
DATABASE_URL="postgresql://tetheruser:password@localhost:5432/tetherdb" DATABASE_URL="postgresql://tetheruser:password@localhost:5432/tetherdb"
JWT_SECRET="" JWT_SECRET=""
COOKIE_SECRET=""

62
package-lock.json generated
View file

@ -1,14 +1,16 @@
{ {
"name": "tether", "name": "tether",
"version": "0.2.0", "version": "0.3.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tether", "name": "tether",
"version": "0.2.0", "version": "0.3.4",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.2.0",
"argon2": "^0.44.0", "argon2": "^0.44.0",
@ -134,6 +136,46 @@
"fast-uri": "^3.0.0" "fast-uri": "^3.0.0"
} }
}, },
"node_modules/@fastify/cookie": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
"integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"cookie": "^1.0.0",
"fastify-plugin": "^5.0.0"
}
},
"node_modules/@fastify/cors": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
"integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"fastify-plugin": "^5.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/error": { "node_modules/@fastify/error": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
@ -1066,6 +1108,22 @@
"toad-cache": "^3.7.0" "toad-cache": "^3.7.0"
} }
}, },
"node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "tether", "name": "tether",
"version": "0.3.4", "version": "0.3.5",
"description": "Communication server using the Nexlink protocol", "description": "Communication server using the Nexlink protocol",
"repository": { "repository": {
"type": "git", "type": "git",
@ -25,6 +25,8 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.2.0",
"argon2": "^0.44.0", "argon2": "^0.44.0",

View file

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `token` on the `Session` table. All the data in the column will be lost.
- Added the required column `cookie` to the `Session` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Session" DROP COLUMN "token",
ADD COLUMN "cookie" TEXT NOT NULL;

View file

@ -59,7 +59,7 @@ model Session {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
owner User @relation(fields: [userId], references: [id], onDelete: Cascade) owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String userId String
token String cookie String
creationDate DateTime @default(now()) creationDate DateTime @default(now())
} }

View file

@ -1,17 +1,23 @@
import { type FastifyReply, type FastifyRequest } from "fastify"; import { type FastifyReply, type FastifyRequest } from "fastify";
import type { import type {
ILoginRequest, IPostLoginRequest,
IRegisterResponseError, IPostRegisterResponseError,
IRegisterResponseSuccess, IPostRegisterResponseSuccess,
IRegisterRequest, IPostRegisterRequest,
ILoginResponseError, IPostLoginResponseError,
ILoginResponseSuccess, IPostLoginResponseSuccess,
IGetRefreshResponseError,
IGetRefreshResponseSuccess,
} from "./types.js"; } from "./types.js";
import { loginUser, registerUser } from "../../services/auth/auth.js"; import {
loginUser,
refreshSession,
registerUser,
} from "../../services/auth/auth.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => { const postRegister = async (request: FastifyRequest, reply: FastifyReply) => {
const { username, password, email } = request.body as IRegisterRequest; const { username, password, email } = request.body as IPostRegisterRequest;
const newUser = await registerUser({ const newUser = await registerUser({
username: username, username: username,
@ -20,20 +26,21 @@ const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
}); });
if (!newUser) { if (!newUser) {
reply.status(409);
return { return {
error: API_ERROR.USER_ALREADY_EXISTS, error: API_ERROR.USER_ALREADY_EXISTS,
} as IRegisterResponseError; } as IPostRegisterResponseError;
} }
return { return {
id: newUser.id, id: newUser.id,
username: newUser.username, username: newUser.username,
registerDate: newUser.registerDate?.getTime(), registerDate: newUser.registerDate?.getTime(),
} as IRegisterResponseSuccess; } as IPostRegisterResponseSuccess;
}; };
const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => { const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
const { username, password } = request.body as ILoginRequest; const { username, password } = request.body as IPostLoginRequest;
const session = await loginUser({ const session = await loginUser({
username: username, username: username,
@ -41,17 +48,44 @@ const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
}); });
if (!session) { if (!session) {
reply.status(403);
return { return {
username: username, username: username,
error: API_ERROR.ACCESS_DENIED, error: API_ERROR.ACCESS_DENIED,
} as ILoginResponseError; } as IPostLoginResponseError;
} }
reply.setCookie("token", session.cookie, {
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
maxAge: 60 * 60 * 24 * 365 * 100,
});
return { return {
id: session.id, id: session.id,
ownerId: session.userId, ownerId: session.userId,
token: session.token, } as IPostLoginResponseSuccess;
} as ILoginResponseSuccess;
}; };
export { postRegister, postLogin }; const getRefresh = async (request: FastifyRequest, reply: FastifyReply) => {
const cookie = request.cookies["token"];
const refresh = await refreshSession(cookie);
if (!refresh) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IGetRefreshResponseError;
}
return {
id: refresh[0].id,
ownerId: refresh[0].userId,
token: refresh[1],
} as IGetRefreshResponseSuccess;
};
export { postRegister, postLogin, getRefresh };

View file

@ -4,6 +4,7 @@ import * as controller from "./auth.js";
const authRoutes = async (fastify: FastifyInstance) => { const authRoutes = async (fastify: FastifyInstance) => {
fastify.post(`/register`, controller.postRegister); fastify.post(`/register`, controller.postRegister);
fastify.post(`/login`, controller.postLogin); fastify.post(`/login`, controller.postLogin);
fastify.get(`/refresh`, controller.getRefresh);
}; };
export { authRoutes }; export { authRoutes };

View file

@ -1,42 +1,53 @@
import type { API_ERROR } from "../errors.js"; import type { API_ERROR } from "../errors.js";
interface IRegisterRequest { interface IPostRegisterRequest {
username: string; username: string;
password: string; password: string;
email?: string; email?: string;
} }
interface IRegisterResponseSuccess { interface IPostRegisterResponseSuccess {
id: string; id: string;
username: string; username: string;
registerDate: number; registerDate: number;
} }
interface IRegisterResponseError { interface IPostRegisterResponseError {
error: API_ERROR; error: API_ERROR;
} }
interface ILoginRequest { interface IPostLoginRequest {
username: string; username: string;
password: string; password: string;
} }
interface ILoginResponseSuccess { interface IPostLoginResponseSuccess {
id: string;
ownerId: string;
}
interface IPostLoginResponseError {
username: string;
error: API_ERROR;
}
interface IGetRefreshResponseSuccess {
id: string; id: string;
ownerId: string; ownerId: string;
token: string; token: string;
} }
interface ILoginResponseError { interface IGetRefreshResponseError {
username: string;
error: API_ERROR; error: API_ERROR;
} }
export { export {
type IRegisterRequest, type IPostRegisterRequest,
type IRegisterResponseSuccess, type IPostRegisterResponseSuccess,
type IRegisterResponseError, type IPostRegisterResponseError,
type ILoginRequest, type IPostLoginRequest,
type ILoginResponseSuccess, type IPostLoginResponseSuccess,
type ILoginResponseError, type IPostLoginResponseError,
type IGetRefreshResponseError,
type IGetRefreshResponseSuccess,
}; };

View file

@ -1,6 +1,4 @@
import { type FastifyReply, type FastifyRequest } from "fastify"; import { type FastifyReply, type FastifyRequest } from "fastify";
import { getDB } from "../../store/store.js";
import { hashPassword } from "../../services/auth/auth.js";
import { getUserFromAuth } from "../../services/auth/helpers.js"; import { getUserFromAuth } from "../../services/auth/helpers.js";
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => { const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
@ -10,42 +8,6 @@ const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => { const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
const authHeader = _request.headers["authorization"]; const authHeader = _request.headers["authorization"];
return [{ user: await getUserFromAuth(authHeader) }]; return [{ user: await getUserFromAuth(authHeader) }];
const owner = await getDB().user.create({
data: {
username: "TestUser",
passwordHash: await hashPassword("29144"),
email: "testuser@aslan2142.space",
admin: true,
},
});
await getDB().community.create({
data: {
name: "TestCommunity",
ownerId: owner.id,
members: {
connect: {
id: owner.id,
},
create: [
{
username: "Aslo",
passwordHash: await hashPassword("pass"),
admin: false,
},
{
username: "Aslan",
passwordHash: await hashPassword("8556"),
email: "aslan@aslan2142.space",
admin: false,
},
],
},
},
});
return [{ message: "ok" }];
}; };
export { getPing, getTest }; export { getPing, getTest };

View file

@ -38,7 +38,7 @@ const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
const user = await getLoggedUserAuth(authHeader); const user = await getLoggedUserAuth(authHeader);
if (user === API_ERROR.ACCESS_DENIED) { if (user === API_ERROR.ACCESS_DENIED) {
reply.status(404); reply.status(403);
return { return {
error: API_ERROR.ACCESS_DENIED, error: API_ERROR.ACCESS_DENIED,
} as IGetLoggedUserResponseError; } as IGetLoggedUserResponseError;

View file

@ -1,6 +1,11 @@
import Fastify from "fastify";
import cors from "@fastify/cors";
import cookie from "@fastify/cookie";
import { config } from "./config.js"; import { config } from "./config.js";
import Fastify from "fastify"; import { getCookieSecret } from "./services/auth/helpers.js";
import { testRoutes } from "./controllers/test/routes.js"; import { testRoutes } from "./controllers/test/routes.js";
import { authRoutes } from "./controllers/auth/routes.js"; import { authRoutes } from "./controllers/auth/routes.js";
import { userRoutes } from "./controllers/user/routes.js"; import { userRoutes } from "./controllers/user/routes.js";
@ -14,6 +19,13 @@ const app = Fastify({
logger: true, logger: true,
}); });
app.register(cors, {
origin: "http://localhost:3000",
credentials: true,
});
app.register(cookie, { secret: getCookieSecret() });
app.register(testRoutes); app.register(testRoutes);
app.register(authRoutes, { prefix: "/api/v1/auth" }); app.register(authRoutes, { prefix: "/api/v1/auth" });
app.register(userRoutes, { prefix: "/api/v1/user" }); app.register(userRoutes, { prefix: "/api/v1/user" });

View file

@ -1,10 +1,12 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import type { User, Session } from "../../generated/prisma/client.js"; import type { User, Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js"; import { getDB } from "../../store/store.js";
import {
createSessionCookie,
createToken,
hashPassword,
verifyPassword,
} from "./helpers.js";
import type { IUserLogin, IUserRegistration } from "./types.js"; import type { IUserLogin, IUserRegistration } from "./types.js";
import { getJwtSecret } from "./helpers.js";
const registerUser = async ( const registerUser = async (
registration: IUserRegistration, registration: IUserRegistration,
@ -50,11 +52,7 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
return null; return null;
} }
const passwordCorrect = await argon2.verify( if (!(await verifyPassword(user.passwordHash, login.password))) {
user.passwordHash,
login.password,
);
if (!passwordCorrect) {
return null; return null;
} }
@ -69,23 +67,30 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
return await getDB().session.create({ return await getDB().session.create({
data: { data: {
token: createToken(user.id), cookie: createSessionCookie(),
userId: user.id, userId: user.id,
}, },
}); });
}; };
const hashPassword = async (password: string): Promise<string> => { const refreshSession = async (
return await argon2.hash(password, { cookie: string | undefined,
type: argon2.argon2id, ): Promise<[Session, string] | null> => {
memoryCost: 2 ** 16, if (!cookie) {
timeCost: 4, return null;
parallelism: 1, }
const session = await getDB().session.findFirst({
where: {
cookie: cookie,
},
}); });
if (!session) {
return null;
}
return [session, createToken(session.id)];
}; };
const createToken = (userId: string) => { export { registerUser, loginUser, refreshSession };
return jwt.sign({ sub: userId }, getJwtSecret());
};
export { registerUser, loginUser, hashPassword };

View file

@ -1,39 +1,60 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import type { import crypto from "crypto";
Community, import type { Community, User } from "../../generated/prisma/client.js";
Session,
User,
} from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js"; import { getDB } from "../../store/store.js";
import type { IOwnerCheck } from "./types.js"; import type { AccessTokenPayload, IOwnerCheck } from "./types.js";
import type { PERMISSION } from "./permission.js"; import type { PERMISSION } from "./permission.js";
const getJwtSecret = () => { const getJwtSecret = (): string => {
return process.env.JWT_SECRET || ""; return process.env.JWT_SECRET || "";
}; };
const verifyToken = (token: string): string | jwt.JwtPayload | null => { const getCookieSecret = (): string => {
return process.env.COOKIE_SECRET || "";
};
const createSessionCookie = () => {
return crypto.randomBytes(32).toString("hex");
};
const createToken = (sessionId: string) => {
return jwt.sign({ sessionId: sessionId }, getJwtSecret());
};
const verifyToken = (token: string): string | null => {
try { try {
return jwt.verify(token, getJwtSecret()); const payload = jwt.verify(token, getJwtSecret()) as AccessTokenPayload;
return payload.sessionId;
} catch { } catch {
return null; return null;
} }
}; };
const getSessionFromToken = async (token: string): Promise<Session | null> => { const hashPassword = async (password: string): Promise<string> => {
return await getDB().session.findFirst({ return await argon2.hash(password, {
where: { type: argon2.argon2id,
token: token, memoryCost: 2 ** 16,
}, timeCost: 4,
parallelism: 1,
}); });
}; };
const getUserFromToken = async (token: string): Promise<User | null> => { const verifyPassword = async (
const session = await getSessionFromToken(token); passwordHash: string,
passwordToCheck: string,
) => {
return await argon2.verify(passwordHash, passwordToCheck);
};
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
return await getDB().user.findFirst({ return await getDB().user.findFirst({
where: { where: {
id: session?.userId ?? "invalid", Session: {
some: {
id: sessionId,
},
},
}, },
}); });
}; };
@ -43,12 +64,12 @@ const getUserFromAuth = async (
): Promise<User | null> => { ): Promise<User | null> => {
const token = authHeader?.replace("Bearer ", ""); const token = authHeader?.replace("Bearer ", "");
const verified = verifyToken(token ?? "") !== null; const sessionId = verifyToken(token ?? "");
if (!verified || !token) { if (!sessionId || !token) {
return null; return null;
} }
const user = await getUserFromToken(token); const user = await getUserBySessionId(sessionId);
if (!user) { if (!user) {
return null; return null;
} }
@ -187,9 +208,13 @@ const isUserInCommunity = async (
export { export {
getJwtSecret, getJwtSecret,
getCookieSecret,
createSessionCookie,
createToken,
verifyToken, verifyToken,
getSessionFromToken, hashPassword,
getUserFromToken, verifyPassword,
getUserBySessionId,
getUserFromAuth, getUserFromAuth,
isUserOwnerOrAdmin, isUserOwnerOrAdmin,
getUserPermissions, getUserPermissions,

View file

@ -1,3 +1,4 @@
import type { JwtPayload } from "jsonwebtoken";
import type { import type {
Channel, Channel,
Community, Community,
@ -6,7 +7,10 @@ import type {
Session, Session,
User, User,
} from "../../generated/prisma/client.js"; } from "../../generated/prisma/client.js";
import type { PERMISSION } from "./permission.js";
interface AccessTokenPayload extends JwtPayload {
sessionId: string;
}
interface IUserRegistration { interface IUserRegistration {
username: string; username: string;
@ -28,4 +32,9 @@ interface IOwnerCheck {
role?: Role | null; role?: Role | null;
} }
export { type IUserRegistration, type IUserLogin, type IOwnerCheck }; export {
type AccessTokenPayload,
type IUserRegistration,
type IUserLogin,
type IOwnerCheck,
};

View file

@ -1,7 +1,14 @@
import assert from "node:assert"; import assert from "node:assert";
import { test } from "node:test"; import { test } from "node:test";
import { validate } from "uuid"; import { validate } from "uuid";
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js"; import {
apiCookie,
apiToken,
apiGet,
apiPost,
apiPatch,
apiDelete,
} from "./api.js";
const state = {}; const state = {};
@ -40,14 +47,25 @@ test("shouldn't be able to login", async () => {
}); });
test("can login", async () => { test("can login", async () => {
const response = await apiPost(`auth/login`, { const responseArray = await apiCookie(`auth/login`, {
username: state.username, username: state.username,
password: state.password, password: state.password,
}); });
const response = responseArray[0];
state.cookie = responseArray[1];
assert.equal(validate(response.id), true);
assert.equal(validate(response.ownerId), true);
assert.equal(response.ownerId, state.userId);
state.sessionId = response.id;
});
test("can get access token", async () => {
const response = await apiToken(`auth/refresh`, state.cookie);
assert.equal(validate(response.id), true); assert.equal(validate(response.id), true);
assert.equal(validate(response.ownerId), true); assert.equal(validate(response.ownerId), true);
assert.equal(response.token.length > 0, true);
assert.equal(response.ownerId, state.userId); assert.equal(response.ownerId, state.userId);
state.sessionId = response.id; state.sessionId = response.id;

View file

@ -1,7 +1,14 @@
import assert from "node:assert"; import assert from "node:assert";
import { test } from "node:test"; import { test } from "node:test";
import { validate } from "uuid"; import { validate } from "uuid";
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js"; import {
apiCookie,
apiToken,
apiGet,
apiPost,
apiPatch,
apiDelete,
} from "./api.js";
const state = {}; const state = {};
@ -30,19 +37,27 @@ test("can create community", async () => {
}); });
state.userId2 = responseRegister2.id; state.userId2 = responseRegister2.id;
const responseLogin1 = await apiPost(`auth/login`, { const responseLogin1Array = await apiCookie(`auth/login`, {
username: state.username1, username: state.username1,
password: state.password1, password: state.password1,
}); });
const responseLogin1 = responseLogin1Array[0];
state.cookie1 = responseLogin1Array[1];
state.sessionId1 = responseLogin1.id; state.sessionId1 = responseLogin1.id;
state.token1 = responseLogin1.token;
const responseLogin2 = await apiPost(`auth/login`, { const responseLogin2Array = await apiCookie(`auth/login`, {
username: state.username2, username: state.username2,
password: state.password2, password: state.password2,
}); });
const responseLogin2 = responseLogin2Array[0];
state.cookie2 = responseLogin2Array[1];
state.sessionId2 = responseLogin2.id; state.sessionId2 = responseLogin2.id;
state.token2 = responseLogin2.token;
const responseRefresh1 = await apiToken(`auth/refresh`, state.cookie1);
state.token1 = responseRefresh1.token;
const responseRefresh2 = await apiToken(`auth/refresh`, state.cookie2);
state.token2 = responseRefresh2.token;
const responseCreate = await apiPost( const responseCreate = await apiPost(
`community`, `community`,

View file

@ -2,6 +2,32 @@ import config from "../src/config.json" with { type: "json" };
const url = `http://localhost:${config.port}/api/v1`; const url = `http://localhost:${config.port}/api/v1`;
const apiCookie = async (endpoint, request) => {
const response = await fetch(`${url}/${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
const responseCookie = response.headers.getSetCookie().at(0) ?? "";
return [await response.json(), responseCookie];
};
const apiToken = async (endpoint, cookie) => {
const response = await fetch(`${url}/${endpoint}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Cookie: cookie,
},
});
return await response.json();
};
const apiGet = async (endpoint, token) => { const apiGet = async (endpoint, token) => {
const response = await fetch(`${url}/${endpoint}`, { const response = await fetch(`${url}/${endpoint}`, {
method: "GET", method: "GET",
@ -53,4 +79,4 @@ const apiDelete = async (endpoint, request, token) => {
return await response.json(); return await response.json();
}; };
export { apiGet, apiPost, apiPatch, apiDelete }; export { apiCookie, apiToken, apiGet, apiPost, apiPatch, apiDelete };