diff --git a/.prettierignore b/.prettierignore index 642271f5..90dab774 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,5 @@ node_modules/ -coverage/ \ No newline at end of file +coverage/ +frontend/.vite/ +keycloak/ +db/ diff --git a/.prettierrc b/.prettierrc index aa22ba87..b200fb85 100644 --- a/.prettierrc +++ b/.prettierrc @@ -8,5 +8,7 @@ "endOfLine": "lf", "semi": true, "bracketSpacing": true, - "quoteProps": "preserve" + "quoteProps": "preserve", + "plugins": ["@ianvs/prettier-plugin-sort-imports"], + "importOrder": ["", "", "", "", "^[.]"] } diff --git a/backend/package-lock.json b/backend/package-lock.json index 8d1a9ba2..0de50cac 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,7 @@ "neo4j-driver": "^5.13.0", "socket.io": "^4.7.2", "unidecode": "^0.1.8", + "uuid": "^9.0.1", "vite": "^5.2.6", "zod": "^3.22.4" }, @@ -36,7 +37,6 @@ "tsc-watch": "^6.0.4", "tsx": "^4.7.1", "typescript": "^5.2.2", - "uuid": "^9.0.1", "vitest": "^0.34.6" } }, @@ -3960,7 +3960,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -6988,8 +6987,7 @@ "uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "v8-to-istanbul": { "version": "9.1.3", diff --git a/backend/package.json b/backend/package.json index 8cdf9c4d..46a6d8a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,6 +27,7 @@ "neo4j-driver": "^5.13.0", "socket.io": "^4.7.2", "unidecode": "^0.1.8", + "uuid": "^9.0.1", "vite": "^5.2.6", "zod": "^3.22.4" }, @@ -42,7 +43,6 @@ "tsc-watch": "^6.0.4", "tsx": "^4.7.1", "typescript": "^5.2.2", - "uuid": "^9.0.1", "vitest": "^0.34.6" } } diff --git a/backend/src/driver/driver.ts b/backend/src/driver.ts similarity index 100% rename from backend/src/driver/driver.ts rename to backend/src/driver.ts diff --git a/backend/src/httpServer.ts b/backend/src/httpServer.ts index 543d8abe..258b25fc 100644 --- a/backend/src/httpServer.ts +++ b/backend/src/httpServer.ts @@ -1,17 +1,38 @@ -import dotenv from "dotenv"; -import servers from "./server.js"; -import usersRouter from "./routes/usersRoute.js"; +import { createServer } from "node:http"; + +import cookieParser from "cookie-parser"; +import cors from "cors"; +import express from "express"; + import authRouter from "./routes/authRoute.js"; import chatRouter from "./routes/chatRoute.js"; -import { cleanUpData, importInitialData } from "./db.js"; +import usersRouter from "./routes/usersRoute.js"; -const { app } = servers; +const app = express(); +const port: number = 5000; -dotenv.config(); +const corsOptions = { + origin: ["http://localhost:5000", "http://localhost:5173"], + optionsSuccessStatus: 200, +}; -cleanUpData(); -importInitialData().then((res) => console.log(res)); +app.use(cors(corsOptions)); +app.use((_req, res, next) => { + res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE"); + res.header("Access-Control-Allow-Headers", "Content-Type"); + next(); +}); +app.use(cookieParser()); +app.use(express.json({ limit: "10mb" })); + +const expressServer = createServer(app); + +expressServer.listen(port, () => { + console.log(`HTTP server running on port ${port}`); +}); app.use("/users", usersRouter); app.use("/auth", authRouter); app.use("/chat", chatRouter); + +export { expressServer }; diff --git a/backend/src/db.ts b/backend/src/importDb.ts similarity index 96% rename from backend/src/db.ts rename to backend/src/importDb.ts index 841a062e..28c09b3b 100644 --- a/backend/src/db.ts +++ b/backend/src/importDb.ts @@ -1,8 +1,7 @@ -import driver from "./driver/driver.js"; - -import userData from "./data/users.js"; -import { registerUser, registerUserSchema } from "./users.js"; +import driver from "./driver.js"; +import userData from "./userData.js"; import { addFriend } from "./userFriends.js"; +import { registerUser, registerUserSchema } from "./users.js"; export async function isDatabaseEmpty() { const session = driver.session(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 1ecbafbf..ef86d879 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,3 +1,19 @@ +import "dotenv/config"; import "./httpServer.js"; import "./socketServer.js"; import "./kcAdminClient.js"; + +import { connect } from "mongoose"; + +import { cleanUpData, importInitialData } from "./importDb.js"; + +cleanUpData(); +importInitialData().then((res) => console.log(res)); + +try { + const uri = process.env.MONGODB_URI || "mongodb://localhost:27017"; + await connect(`${uri}/chats`); + console.log("Chat database started"); +} catch (err) { + console.error(err); +} diff --git a/backend/src/kcAdminClient.ts b/backend/src/kcAdminClient.ts index 89eb6a5a..d8e99a94 100644 --- a/backend/src/kcAdminClient.ts +++ b/backend/src/kcAdminClient.ts @@ -2,6 +2,9 @@ import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; export const keycloakUri = process.env.KEYCLOAK_URI || "http://localhost:3000"; export const keycloakIssuer = process.env.KEYCLOAK_ISSUER || keycloakUri; +export const keycloakCredentials = Buffer.from( + `mercury-backend:${process.env.CLIENT_SECRET}`, +).toString("base64"); const kcAdminClient = new KeycloakAdminClient({ baseUrl: keycloakUri, diff --git a/backend/src/meetings.ts b/backend/src/meetings.ts index 317ef34b..ec346ab1 100644 --- a/backend/src/meetings.ts +++ b/backend/src/meetings.ts @@ -1,5 +1,6 @@ import { Session } from "neo4j-driver"; import { v4 as uuidv4 } from "uuid"; + import Meeting from "./models/Meeting.js"; export async function isInMeeting(session: Session, userId: string) { diff --git a/backend/src/misc/fetchData.ts b/backend/src/misc/fetchData.ts index 4937b1a3..4b2d161f 100644 --- a/backend/src/misc/fetchData.ts +++ b/backend/src/misc/fetchData.ts @@ -1,4 +1,16 @@ -export const fetchData = async (url: string, method: string, options = {}) => { +export const fetchData = async ( + url: string, + method: string, + options: RequestInit = {}, + token?: string, +) => { + if (token) { + options.headers = { + ...options.headers, + "Authorization": `Bearer ${token}`, + }; + } + try { const response = await fetch(url, { ...options, method }); const data = await response.json(); diff --git a/backend/src/misc/formatError.ts b/backend/src/misc/formatError.ts index c00c1c00..3682afc3 100644 --- a/backend/src/misc/formatError.ts +++ b/backend/src/misc/formatError.ts @@ -1,4 +1,5 @@ import { ZodError } from "zod"; + import { Errors } from "../models/Response.js"; export function formatError(error: ZodError): Errors { diff --git a/backend/src/misc/jwt.ts b/backend/src/misc/jwt.ts index d4eb8a81..2c388abe 100644 --- a/backend/src/misc/jwt.ts +++ b/backend/src/misc/jwt.ts @@ -1,17 +1,22 @@ -import { Request, Response, NextFunction } from "express"; +import { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; -import { AuthResponse, CustomResponse } from "../models/Response.js"; + +import { + keycloakCredentials, + keycloakIssuer, + keycloakUri, +} from "../kcAdminClient.js"; import DecodedData from "../models/DecodedData.js"; import Issuer from "../models/Issuer.js"; +import { AuthResponse, CustomResponse } from "../models/Response.js"; import TokenPayload from "../models/TokenPayload.js"; -import { keycloakIssuer, keycloakUri } from "../kcAdminClient.js"; export interface JWTRequest extends Request { token?: TokenPayload; tokenStr?: string; } -const issuers: Record = { +export const issuers: Record = { mercury: `${keycloakIssuer}/realms/mercury`, rest: "http://localhost:5000", }; @@ -37,12 +42,14 @@ function tokenIssuerToName(issuer: string): Issuer | "unknown" { export async function verifyKeycloakToken(tokenStr: string): Promise { const response = await fetch( - `${keycloakUri}/realms/mercury/protocol/openid-connect/userinfo`, + `${keycloakUri}/realms/mercury/protocol/openid-connect/token/introspect`, { - method: "GET", + method: "POST", headers: { - "Authorization": `Bearer ${tokenStr}`, + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": `Basic ${keycloakCredentials}`, }, + body: `token=${tokenStr}`, }, ); diff --git a/backend/src/models/ChangePasswordReq.ts b/backend/src/models/ChangePasswordReq.ts index 04665b5c..a55534e6 100644 --- a/backend/src/models/ChangePasswordReq.ts +++ b/backend/src/models/ChangePasswordReq.ts @@ -1,4 +1,5 @@ import { z } from "zod"; + import { userPasswordSchema } from "./User.js"; type ChangePasswordReq = diff --git a/backend/src/models/NativeUser.ts b/backend/src/models/NativeUser.ts index 471733c7..3730d888 100644 --- a/backend/src/models/NativeUser.ts +++ b/backend/src/models/NativeUser.ts @@ -1,4 +1,4 @@ -import { ZodType, z } from "zod"; +import { z, ZodType } from "zod"; type NativeUser = { password: string; diff --git a/backend/src/models/Response.ts b/backend/src/models/Response.ts index d0f80c37..36005773 100644 --- a/backend/src/models/Response.ts +++ b/backend/src/models/Response.ts @@ -1,7 +1,8 @@ -import User from "./User.js"; - import { Response } from "express"; +import Message from "./Message.js"; +import User from "./User.js"; + type Send = (body?: J) => T; export interface CustomResponse extends Response { @@ -60,6 +61,11 @@ export interface UsersSearchResponse { users: User[]; } +export interface MessagesResponse { + status: "ok"; + messages: Message[]; +} + export interface JWTResponse extends OkResponse { token: string; } diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 836a8ad6..5d12d17f 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -1,4 +1,4 @@ -import { ZodType, z } from "zod"; +import { z, ZodType } from "zod"; export default interface User { id: string; diff --git a/backend/src/models/routes/Page.ts b/backend/src/models/routes/Page.ts index 259afee9..b436bbea 100644 --- a/backend/src/models/routes/Page.ts +++ b/backend/src/models/routes/Page.ts @@ -1,4 +1,4 @@ -import { ZodType, z } from "zod"; +import { z, ZodType } from "zod"; interface Page { page: string | number; diff --git a/backend/src/models/routes/Search.ts b/backend/src/models/routes/Search.ts index 38b51b71..476ff38b 100644 --- a/backend/src/models/routes/Search.ts +++ b/backend/src/models/routes/Search.ts @@ -1,6 +1,7 @@ -import { ZodType, z } from "zod"; -import Page, { pageSchema } from "./Page.js"; +import { z, ZodType } from "zod"; + import { userCountrySchema } from "../User.js"; +import Page, { pageSchema } from "./Page.js"; interface Search extends Page { q: string; diff --git a/backend/src/mongoDB/MessageModel.ts b/backend/src/mongoDB/MessageModel.ts index 816a37cb..6cdce3bc 100644 --- a/backend/src/mongoDB/MessageModel.ts +++ b/backend/src/mongoDB/MessageModel.ts @@ -1,4 +1,5 @@ import { model, Schema } from "mongoose"; + import Message from "../models/Message.js"; const chatSchema = new Schema({ diff --git a/backend/src/routes/authRoute.ts b/backend/src/routes/authRoute.ts index 9af001d9..e6ea581c 100644 --- a/backend/src/routes/authRoute.ts +++ b/backend/src/routes/authRoute.ts @@ -1,21 +1,19 @@ -import { Router, Request } from "express"; - import bcrypt from "bcrypt"; +import { Request, Router } from "express"; +import jwt, { JwtPayload } from "jsonwebtoken"; -import driver from "../driver/driver.js"; +import driver from "../driver.js"; +import { leaveMeeting } from "../meetings.js"; import { - JWTRequest, authenticateToken, generateAccessToken, generateRefreshToken, + JWTRequest, } from "../misc/jwt.js"; - -import jwt, { JwtPayload } from "jsonwebtoken"; +import { Errors } from "../models/Response.js"; import { TokenErrorResponse } from "../types/authResponse.js"; import { OkErrorResponse } from "../types/userResponse.js"; -import { leaveMeeting } from "../meetings.js"; import { getDbUser } from "../users.js"; -import { Errors } from "../models/Response.js"; const authRouter = Router(); diff --git a/backend/src/routes/chatRoute.ts b/backend/src/routes/chatRoute.ts index c8538383..dfb8d6bc 100644 --- a/backend/src/routes/chatRoute.ts +++ b/backend/src/routes/chatRoute.ts @@ -1,35 +1,59 @@ import { Router } from "express"; + +import driver from "../driver.js"; +import { authenticateToken, JWTRequest } from "../misc/jwt.js"; import Message from "../models/Message.js"; +import { Errors } from "../models/Response.js"; import MessageModel from "../mongoDB/MessageModel.js"; +import { AuthMessagesErrorResponse } from "../types/userResponse.js"; +import { getTokenDbUser } from "../users.js"; +import { userNotFoundRes } from "./usersRoute.js"; const chatRouter = Router(); -chatRouter.get("/:user1Id/:user2Id", async (req, res) => { - try { - const { user1Id, user2Id } = req.params; - const messageRequest = await MessageModel.find({ - $or: [ - { fromUserId: user1Id, toUserId: user2Id }, - { fromUserId: user2Id, toUserId: user1Id }, - ], - }).sort({ created_date: "asc" }); - - const messages = messageRequest.map((m: Message) => { - const { sentDate, fromUserId, toUserId, content } = m; - return { - type: fromUserId === user1Id ? "sent" : "received", - sentDate, - fromUserId, - toUserId, - content, - }; - }); - - return res.json({ status: "ok", messages }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } -}); +chatRouter.get( + "/:user1Id/:user2Id", + authenticateToken, + async (req: JWTRequest, res: AuthMessagesErrorResponse) => { + const session = driver.session(); + try { + const { user1Id, user2Id } = req.params; + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != user1Id) { + return res.status(403).json({ status: "forbidden" }); + } + + const messageRequest = await MessageModel.find({ + $or: [ + { fromUserId: user1Id, toUserId: user2Id }, + { fromUserId: user2Id, toUserId: user1Id }, + ], + }).sort({ created_date: "asc" }); + + const messages = messageRequest.map((m: Message) => { + const { sentDate, fromUserId, toUserId, content } = m; + return { + type: fromUserId === user1Id ? "sent" : "received", + sentDate, + fromUserId, + toUserId, + content, + } as Message; + }); + + return res.json({ status: "ok", messages }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + session.close(); + } + }, +); export default chatRouter; diff --git a/backend/src/routes/userFriendsRoute.ts b/backend/src/routes/userFriendsRoute.ts index a7f6dc55..656cadcc 100644 --- a/backend/src/routes/userFriendsRoute.ts +++ b/backend/src/routes/userFriendsRoute.ts @@ -1,49 +1,64 @@ -import { Router, Request, Response } from "express"; +import { Request, Response, Router } from "express"; import { Session } from "neo4j-driver"; -import driver from "../driver/driver.js"; + +import driver from "../driver.js"; +import { formatError } from "../misc/formatError.js"; +import { authenticateToken, JWTRequest } from "../misc/jwt.js"; +import { Errors } from "../models/Response.js"; +import Page, { pageSchema } from "../models/routes/Page.js"; import User from "../models/User.js"; import { - OkErrorResponse, - FriendsPageErrorResponse, + AuthOkErrorResponse, FriendRequestsPageErrorResponse, + FriendsPageErrorResponse, FriendSuggestionsPageErrorResponse, + OkErrorResponse, } from "../types/userResponse.js"; -import { deleteFriend } from "../userFriends.js"; -import { declineFriendRequest } from "../userFriends.js"; -import { acceptFriendRequest } from "../userFriends.js"; -import { sendFriendRequest } from "../userFriends.js"; -import { getFriendSuggestionsCount } from "../userFriends.js"; -import { getFriendSuggestions } from "../userFriends.js"; -import { getFriendRequestsCount } from "../userFriends.js"; -import { getFriendRequests } from "../userFriends.js"; -import { getFriendsCount } from "../userFriends.js"; -import { getFriends } from "../userFriends.js"; +import { + acceptFriendRequest, + declineFriendRequest, + deleteFriend, + getFriendRequests, + getFriendRequestsCount, + getFriends, + getFriendsCount, + getFriendSuggestions, + getFriendSuggestionsCount, + sendFriendRequest, +} from "../userFriends.js"; +import { getDbUser, getTokenDbUser } from "../users.js"; import { userNotFoundRes } from "./usersRoute.js"; -import { Errors } from "../models/Response.js"; -import Page, { pageSchema } from "../models/routes/Page.js"; -import { formatError } from "../misc/formatError.js"; const friendsRouter = Router(); friendsRouter.get( "/:userId/friends", - async (req: Request, res: FriendsPageErrorResponse) => { - const userId = req.params.userId; + authenticateToken, + async (req: JWTRequest, res: FriendsPageErrorResponse) => { + const session = driver.session(); + try { + const userId = req.params.userId; + const user = await getTokenDbUser(session, req.token!); - const pageParse = pageSchema.safeParse(req.query); - if (!pageParse.success) { - const errors = formatError(pageParse.error); - return res.status(400).json({ status: "error", errors }); - } + if (!user) { + return userNotFoundRes(res); + } - const { page, maxUsers }: Page = pageParse.data; - const maxUsersBig = BigInt(maxUsers); + if (user.id != userId) { + return res.status(403).json({ status: "forbidden" }); + } + + const pageParse = pageSchema.safeParse(req.query); + if (!pageParse.success) { + const errors = formatError(pageParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const { page, maxUsers }: Page = pageParse.data; + const maxUsersBig = BigInt(maxUsers); - const session = driver.session(); - try { const friends = await getFriends(session, userId, page - 1, maxUsers); if (friends === null) { - console.log(friends); return userNotFoundRes(res); } @@ -55,7 +70,6 @@ friendsRouter.get( const pageCount = Number( (friendsCount.toBigInt() + maxUsersBig - 1n) / maxUsersBig, ); - console.log(pageCount); return res.json({ status: "ok", pageCount, friends }); } catch (err) { console.log("Error:", err); @@ -112,20 +126,31 @@ friendsRouter.get( friendsRouter.get( "/:userId/friend-suggestions", - async (req: Request, res: FriendSuggestionsPageErrorResponse) => { + authenticateToken, + async (req: JWTRequest, res: FriendSuggestionsPageErrorResponse) => { const userId = req.params.userId; - const pageParse = pageSchema.safeParse(req.query); - if (!pageParse.success) { - const errors = formatError(pageParse.error); - return res.status(400).json({ status: "error", errors }); - } - - const { page, maxUsers }: Page = pageParse.data; - const maxUsersBig = BigInt(maxUsers); - const session = driver.session(); try { + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != userId) { + return res.status(403).json({ status: "forbidden" }); + } + + const pageParse = pageSchema.safeParse(req.query); + if (!pageParse.success) { + const errors = formatError(pageParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const { page, maxUsers }: Page = pageParse.data; + const maxUsersBig = BigInt(maxUsers); + const friendSuggestions = await getFriendSuggestions( session, userId, @@ -159,12 +184,23 @@ friendsRouter.get( friendsRouter.post( "/:userId1/send-friend-request/:userId2", - async (req: Request, res: OkErrorResponse) => { + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { const session = driver.session(); const userId1 = req.params.userId1; const userId2 = req.params.userId2; try { + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != userId1) { + return res.status(403).json({ status: "forbidden" }); + } + const requestResult = await sendFriendRequest(session, userId1, userId2); if (!requestResult.success) { const { firstUserExists, secondUserExists } = requestResult; @@ -193,12 +229,23 @@ friendsRouter.post( friendsRouter.post( "/:userId1/accept-friend-request/:userId2", - async (req: Request, res: OkErrorResponse) => { + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { const session = driver.session(); const userId1 = req.params.userId1; const userId2 = req.params.userId2; try { + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != userId1) { + return res.status(403).json({ status: "forbidden" }); + } + const requestResult = await acceptFriendRequest( session, userId1, @@ -244,12 +291,23 @@ friendsRouter.post( friendsRouter.post( "/:userId1/decline-friend-request/:userId2", - async (req: Request, res: OkErrorResponse) => { + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { const session = driver.session(); const userId1 = req.params.userId1; const userId2 = req.params.userId2; try { + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != userId1) { + return res.status(403).json({ status: "forbidden" }); + } + const requestResult = await declineFriendRequest( session, userId1, @@ -291,12 +349,23 @@ friendsRouter.post( friendsRouter.delete( "/:userId1/delete-friend/:userId2", - async (req: Request, res: OkErrorResponse) => { + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { const session = driver.session(); const userId1 = req.params.userId1; const userId2 = req.params.userId2; try { + const user = await getTokenDbUser(session, req.token!); + + if (!user) { + return userNotFoundRes(res); + } + + if (user.id != userId1) { + return res.status(403).json({ status: "forbidden" }); + } + const requestResult = await deleteFriend(session, userId1, userId2); if (!requestResult.success) { const { firstUserExists, secondUserExists, wasFriend } = requestResult; diff --git a/backend/src/routes/usersRoute.ts b/backend/src/routes/usersRoute.ts index 9fe8b1ad..2c0ac793 100644 --- a/backend/src/routes/usersRoute.ts +++ b/backend/src/routes/usersRoute.ts @@ -1,6 +1,12 @@ -import { Router, Request, Response } from "express"; -import driver from "../driver/driver.js"; -import { JWTRequest, authenticateToken, getToken } from "../misc/jwt.js"; +import { Request, Response, Router } from "express"; + +import driver from "../driver.js"; +import { formatError } from "../misc/formatError.js"; +import { authenticateToken, getToken, JWTRequest } from "../misc/jwt.js"; +import { changePasswordReqSchema } from "../models/ChangePasswordReq.js"; +import DbUser from "../models/DbUser.js"; +import { Errors } from "../models/Response.js"; +import { searchSchema } from "../models/routes/Search.js"; import { AuthOkErrorResponse, OkErrorResponse, @@ -8,29 +14,25 @@ import { UsersErrorResponse, UsersSearchErrorResponse, } from "../types/userResponse.js"; -import usersFriendsRoute from "./userFriendsRoute.js"; import { - getAllUsers, - searchUser as searchUsers, - getUser as getUser, + changePassword, createUser, - updateUser, deleteUser, - UserCreateResult, - registerUser, + getAllUsers, getDbUser, - changePassword, + getTokenDbUser, + getUser, getUsersCount, - registerUserSchema, + registerUser, RegisterUser, - updateUserSchema, + registerUserSchema, + searchUser as searchUsers, + updateUser, UpdateUser, + updateUserSchema, + UserCreateResult, } from "../users.js"; -import DbUser from "../models/DbUser.js"; -import { changePasswordReqSchema } from "../models/ChangePasswordReq.js"; -import { formatError } from "../misc/formatError.js"; -import { Errors } from "../models/Response.js"; -import { searchSchema } from "../models/routes/Search.js"; +import usersFriendsRoute from "./userFriendsRoute.js"; const usersRouter = Router(); @@ -136,55 +138,6 @@ usersRouter.get("/:userId", async (req: Request, res: UserErrorResponse) => { } }); -usersRouter.get("/meetings/:userId", async (req: Request, res) => { - try { - const session = driver.session(); - const userId = req.params.userId; - - const user = await getDbUser(session, { id: userId }); - if (!user) { - await session.close(); - return res; - } - - const meetingsRequest = await session.run( - `MATCH (u1:User {id: $userId})-[m:MEETING]-(u2:User) RETURN m, u2`, - { userId }, - ); - await session.close(); - try { - const meetings = meetingsRequest.records.map((meeting) => { - const { meetingId, waiting } = meeting.get(0).properties; - const { id, first_name, last_name } = meeting.get(1).properties; - return { meetingId, id, first_name, last_name, waiting }; - }); - return res.json({ status: "ok", meetings }); - } catch (_err) { - return res.json({ status: "ok", meetings: [] }); - } - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } -}); - -usersRouter.put("/meetings/:meetingId", async (req: Request, res) => { - try { - const session = driver.session(); - const meetingId = req.params.meetingId; - - await session.run( - `MATCH (u1:User)-[m:MEETING]-(u2:User) WHERE m.meetingId=$meetingId SET m.waiting = true`, - { meetingId }, - ); - await session.close(); - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } -}); - usersRouter.post("/", async (req: Request, res: UserErrorResponse) => { const userParse = registerUserSchema.safeParse(req.body); if (!userParse.success) { @@ -218,31 +171,45 @@ usersRouter.post("/", async (req: Request, res: UserErrorResponse) => { } }); -usersRouter.put("/:userId", async (req: Request, res: OkErrorResponse) => { - const userParse = updateUserSchema.safeParse(req.body); - if (!userParse.success) { - const errors = formatError(userParse.error); - return res.status(400).json({ status: "error", errors }); - } +usersRouter.put( + "/:userId", + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { + const session = driver.session(); + try { + const userId = req.params.userId; + const user = await getTokenDbUser(session, req.token!); - const parsedUser: UpdateUser = userParse.data; - const userId = req.params.userId; + if (!user) { + return userNotFoundRes(res); + } - const session = driver.session(); - try { - const newUser = await updateUser(session, userId, parsedUser); - if (!newUser) { - return userNotFoundRes(res); - } + if (user.id != userId) { + return res.status(403).json({ status: "forbidden" }); + } - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as Errors }); - } finally { - await session.close(); - } -}); + const userParse = updateUserSchema.safeParse(req.body); + if (!userParse.success) { + const errors = formatError(userParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const parsedUser: UpdateUser = userParse.data; + + const newUser = await updateUser(session, userId, parsedUser); + if (!newUser) { + return userNotFoundRes(res); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + await session.close(); + } + }, +); usersRouter.post( "/:userId/change-password", @@ -296,23 +263,36 @@ usersRouter.post( }, ); -usersRouter.delete("/:userId", async (req: Request, res: OkErrorResponse) => { - const userId = req.params.userId; +usersRouter.delete( + "/:userId", + authenticateToken, + async (req: JWTRequest, res: AuthOkErrorResponse) => { + const session = driver.session(); + try { + const userId = req.params.userId; + const user = await getTokenDbUser(session, req.token!); - const session = driver.session(); - try { - const isDeleted = await deleteUser(session, userId); - if (!isDeleted) { - return userNotFoundRes(res); - } + if (!user) { + return userNotFoundRes(res); + } - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as Errors }); - } finally { - await session.close(); - } -}); + if (user.id != userId) { + return res.status(403).json({ status: "forbidden" }); + } + + const isDeleted = await deleteUser(session, userId); + if (!isDeleted) { + return userNotFoundRes(res); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + await session.close(); + } + }, +); export default usersRouter; diff --git a/backend/src/server.ts b/backend/src/server.ts deleted file mode 100644 index 633268e2..00000000 --- a/backend/src/server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import express, { Express } from "express"; -import dotenv from "dotenv"; -import cors from "cors"; -import { Server as SocketServer } from "socket.io"; -import { createServer } from "http"; -import { connect } from "mongoose"; -import cookieParser from "cookie-parser"; -import ServerToClientEvents from "./events/ServerToClientEvents.js"; -import ClientToServerEvents from "./events/ClientToServerEvents.js"; - -dotenv.config(); - -const corsOptions = { - origin: ["http://localhost:5000", "http://localhost:5173"], - optionsSuccessStatus: 200, -}; - -const app: Express = express(); -const port: number = 5000; - -app.use(cors(corsOptions)); -app.use((_req, res, next) => { - res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE"); - res.header("Access-Control-Allow-Headers", "Content-Type"); - next(); -}); -app.use(cookieParser()); -app.use(express.json({ limit: "10mb" })); - -const expressServer = createServer(app); - -const io = new SocketServer( - expressServer, - { - cors: { origin: ["http://localhost:5173"] }, - }, -); - -(async () => { - try { - const uri = process.env.MONGODB_URI || "mongodb://localhost:27017"; - await connect(`${uri}/chats`); - - expressServer.listen(port, () => { - console.log("Chat database started"); - console.log(`HTTP server running on port ${port}`); - }); - } catch (err) { - console.error(err); - } -})(); - -export default { expressServer, io, app }; diff --git a/backend/src/socketServer.ts b/backend/src/socketServer.ts index 5b42ef8f..94454e0b 100644 --- a/backend/src/socketServer.ts +++ b/backend/src/socketServer.ts @@ -1,25 +1,30 @@ -import servers from "./server.js"; -import dotenv from "dotenv"; -import driver from "./driver/driver.js"; -import { Socket } from "socket.io"; -import { - connectToSocket, - disconnectFromSocket, - getAllSockets, -} from "./sockets.js"; -import { isFriend } from "./userFriends.js"; -import Meeting from "./models/Meeting.js"; +import { Socket, Server as SocketServer } from "socket.io"; + +import driver from "./driver.js"; +import ClientToServerEvents from "./events/ClientToServerEvents.js"; +import ServerToClientEvents from "./events/ServerToClientEvents.js"; +import { expressServer } from "./httpServer.js"; import { createMeeting, - leaveMeeting, isInMeeting, joinMeeting, + leaveMeeting, } from "./meetings.js"; import { addMessageToDb } from "./messages.js"; +import Meeting from "./models/Meeting.js"; +import { + connectToSocket, + disconnectFromSocket, + getAllSockets, +} from "./sockets.js"; +import { isFriend } from "./userFriends.js"; -const { io } = servers; - -dotenv.config(); +const io = new SocketServer( + expressServer, + { + cors: { origin: ["http://localhost:5173"] }, + }, +); const meetings: Record = {}; diff --git a/backend/src/types/authResponse.ts b/backend/src/types/authResponse.ts index 4e7899e0..4d514f1a 100644 --- a/backend/src/types/authResponse.ts +++ b/backend/src/types/authResponse.ts @@ -1,8 +1,8 @@ import { - CustomResponse, - JWTResponse, AuthResponse, + CustomResponse, ErrorResponse, + JWTResponse, } from "../models/Response.js"; export type TokenErrorResponse = CustomResponse< diff --git a/backend/src/types/userResponse.ts b/backend/src/types/userResponse.ts index 155d087a..7ff0126a 100644 --- a/backend/src/types/userResponse.ts +++ b/backend/src/types/userResponse.ts @@ -1,15 +1,16 @@ import { + AuthResponse, CustomResponse, ErrorResponse, + FriendRequestsPageResponse, + FriendsPageResponse, + FriendsResponse, + FriendSuggestionsPageResponse, + MessagesResponse, OkResponse, UserResponse, UsersResponse, - FriendsResponse, UsersSearchResponse, - AuthResponse, - FriendsPageResponse, - FriendRequestsPageResponse, - FriendSuggestionsPageResponse, } from "../models/Response.js"; export type UsersErrorResponse = CustomResponse; @@ -25,11 +26,15 @@ export type FriendRequestsPageErrorResponse = CustomResponse< FriendRequestsPageResponse | ErrorResponse >; export type FriendSuggestionsPageErrorResponse = CustomResponse< - FriendSuggestionsPageResponse | ErrorResponse + AuthResponse | FriendSuggestionsPageResponse | ErrorResponse >; export type FriendsPageErrorResponse = CustomResponse< - FriendsPageResponse | ErrorResponse + AuthResponse | FriendsPageResponse | ErrorResponse >; export type UsersSearchErrorResponse = CustomResponse< UsersSearchResponse | ErrorResponse >; + +export type AuthMessagesErrorResponse = CustomResponse< + AuthResponse | MessagesResponse | ErrorResponse +>; diff --git a/backend/src/data/users.ts b/backend/src/userData.ts similarity index 100% rename from backend/src/data/users.ts rename to backend/src/userData.ts diff --git a/backend/src/userFriends.ts b/backend/src/userFriends.ts index 30ae7f2c..dbeac05d 100644 --- a/backend/src/userFriends.ts +++ b/backend/src/userFriends.ts @@ -1,4 +1,5 @@ import neo4j, { Session } from "neo4j-driver"; + import User from "./models/User.js"; import { filterUser, getUser } from "./users.js"; diff --git a/backend/src/users.ts b/backend/src/users.ts index 9e5674a9..e2426720 100644 --- a/backend/src/users.ts +++ b/backend/src/users.ts @@ -1,19 +1,21 @@ +import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; import neo4j, { Session } from "neo4j-driver"; import { v4 as uuidv4 } from "uuid"; -import bcrypt from "bcrypt"; -import User, { userSchema } from "./models/User.js"; +import { ZodType } from "zod"; + +import kcAdminClient from "./kcAdminClient.js"; +import { Either } from "./misc/Either.js"; +import { issuers } from "./misc/jwt.js"; import removeKeys from "./misc/removeKeys.js"; import wordToVec from "./misc/wordToVec.js"; +import ChangePasswordReq from "./models/ChangePasswordReq.js"; import DbUser from "./models/DbUser.js"; -import kcAdminClient from "./kcAdminClient.js"; -import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js"; -import { Either } from "./misc/Either.js"; -import NativeUser, { nativeUserSchema } from "./models/NativeUser.js"; import ExternalUser from "./models/ExternalUser.js"; -import { ZodType } from "zod"; -import ChangePasswordReq from "./models/ChangePasswordReq.js"; -import jwt from "jsonwebtoken"; +import NativeUser, { nativeUserSchema } from "./models/NativeUser.js"; import TokenPayload from "./models/TokenPayload.js"; +import User, { userSchema } from "./models/User.js"; export const filterUser = (user: DbUser): User => { if ("password" in user) { @@ -206,6 +208,41 @@ export async function getDbUser( return user; } +type SubProps = { + sub?: string; + props?: Partial; +}; + +function tokenPayloadToSubProps(tokenPayload: TokenPayload): SubProps { + const issuer = tokenPayload.iss; + + if (issuer == issuers.mercury) { + const sub = tokenPayload.sub; + const props = { issuer: "mercury", issuer_id: sub }; + return { sub, props }; + } else if (issuer == issuers.rest) { + const sub = tokenPayload.userId; + const props = { id: sub }; + return { sub, props }; + } + + return {}; +} + +export async function getTokenDbUser( + session: Session, + tokenPayload: TokenPayload, +): Promise { + let { sub, props } = tokenPayloadToSubProps(tokenPayload); + + if (!sub || !props) { + return null; + } + + const user = await getDbUser(session, props); + return user; +} + export async function getAllUsers(session: Session) { const usersRequest = await session.run(`MATCH (u:User) RETURN u`); const users = usersRequest.records.map((r) => diff --git a/backend/test/chat.test.ts b/backend/test/chat.test.ts index c81ba53d..2234f7e7 100644 --- a/backend/test/chat.test.ts +++ b/backend/test/chat.test.ts @@ -1,40 +1,88 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; import User from "../src/models/User.js"; let userId1: string = ""; let userId2: string = ""; +const userMail1 = "bconford2@wikimedia.org"; +const userPassword1 = "heuristic"; + +const userMail2 = "cruckman3@archive.org"; +const userPassword2 = "coreCar0l;"; + const getUsers = async () => { const response = await fetchData(`http://localhost:5000/users`, "GET", {}); - userId1 = response.users.find( - (user: User) => user.mail === "bconford2@wikimedia.org", - ).id; - userId2 = response.users.find( - (user: User) => user.mail === "cruckman3@archive.org", - ).id; + userId1 = response.users.find((user: User) => user.mail === userMail1).id; + userId2 = response.users.find((user: User) => user.mail === userMail2).id; }; -await getUsers(); +const getKeycloakToken = async ( + mail: string, + password: string, +): Promise => { + const urlParams = new URLSearchParams({ + grant_type: "password", + client_id: "mercury-testing", + client_secret: "5mwGU0Efyh3cT2WVX7ffA8UAWEAmrBag", + username: mail, + password: password, + }); -test("Get chat without ids", async () => { - const response = await fetch(`http://localhost:5000/chat`); - const data = response.headers.get("content-type"); + const response = await fetchData( + `http://localhost:3000/realms/mercury/protocol/openid-connect/token`, + "POST", + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: urlParams, + }, + ); - expect(response.status).toBe(404); - expect(data).toBe("text/html; charset=utf-8"); -}); + return response.access_token; +}; -test("Get chat with ids", async () => { +await getUsers(); +const token1 = await getKeycloakToken(userMail1, userPassword1); +const token2 = await getKeycloakToken(userMail2, userPassword2); + +const getChat = async (userId1: string, userId2: string, token?: string) => { const response = await fetchData( `http://localhost:5000/chat/${userId1}/${userId2}`, "GET", {}, + token, ); - const { status, messages } = response; + return response; +}; + +describe("Get chat messages", () => { + test("without IDs", async () => { + const response = await fetch(`http://localhost:5000/chat`); + const data = response.headers.get("content-type"); + + expect(response.status).toBe(404); + expect(data).toBe("text/html; charset=utf-8"); + }); + + test("without token", async () => { + const { status } = await getChat(userId1, userId2); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await getChat(userId1, userId2, token2); + expect(status).toBe("forbidden"); + }); + + test("correct", async () => { + const { status, messages } = await getChat(userId1, userId2, token1); - expect(status).toBe("ok"); - expect(messages).toBeDefined(); - expect(messages.length).toBe(0); + expect(status).toBe("ok"); + expect(messages).toBeDefined(); + expect(messages.length).toBe(0); + }); }); diff --git a/backend/test/unit.test.ts b/backend/test/unit.test.ts index 031aba31..da34acb4 100644 --- a/backend/test/unit.test.ts +++ b/backend/test/unit.test.ts @@ -1,20 +1,19 @@ import { expect, test } from "vitest"; -import { - letterToKb, - wordDifference, + +import removeKeys from "../src/misc/removeKeys.js"; +import wordToVec, { cosineSimilarity, - sortLetters, + dot, keepLetters, + l2Norm, lerp, - wordVecInterp, + letterToKb, + sortLetters, sum, + wordDifference, + wordVecInterp, zip, - l2Norm, - dot, } from "../src/misc/wordToVec.js"; -import removeKeys from "../src/misc/removeKeys.js"; - -import wordToVec from "../src/misc/wordToVec.js"; test("Letter to Kb", async () => { expect(letterToKb("Q")).toStrictEqual(-1); diff --git a/backend/test/userCRUD.test.ts b/backend/test/userCRUD.test.ts index ea26cade..0ee89d4e 100644 --- a/backend/test/userCRUD.test.ts +++ b/backend/test/userCRUD.test.ts @@ -1,7 +1,8 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; -let userId: number; +let userId: string; let userData = { first_name: "John", last_name: "Smith", @@ -10,306 +11,339 @@ let userData = { mail: "john_smith@example.com", password: "12345678", }; -let token: string; -const login = async (mail: string, password: string) => { - const response = await fetchData(`http://localhost:5000/auth/login`, "POST", { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ mail: mail, password: password }), +const userMail2 = "cruckman3@archive.org"; +const userPassword2 = "coreCar0l;"; + +const getKeycloakToken = async ( + mail: string, + password: string, +): Promise => { + const urlParams = new URLSearchParams({ + grant_type: "password", + client_id: "mercury-testing", + client_secret: "5mwGU0Efyh3cT2WVX7ffA8UAWEAmrBag", + username: mail, + password: password, }); - token = response.token; -}; - -test("Get all users", async () => { - const response = await fetchData(`http://localhost:5000/users`, "GET", {}); - const { status, users } = response; - - expect(status).toBe("ok"); - expect(users.length).toBe(27); -}); - -test("Create user", async () => { - const response = await fetchData(`http://localhost:5000/users`, "POST", { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userData), - }); - - const { status, user } = response; - - expect(status).toBe("ok"); - expect(user.first_name).toBe(userData.first_name); - expect(user.last_name).toBe(userData.last_name); - expect(user.mail).toBe(userData.mail); - - userId = user.id; -}); - -test("Create user with existing mail", async () => { - userData.mail = "shudghton1@geocities.com"; - - const response = await fetchData(`http://localhost:5000/users`, "POST", { - headers: { - "Content-Type": "application/json", + const response = await fetchData( + `http://localhost:3000/realms/mercury/protocol/openid-connect/token`, + "POST", + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: urlParams, }, - body: JSON.stringify(userData), - }); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.id).toBe("already exists"); -}); + ); -test("Create user with short first name", async () => { - userData.first_name = "j"; + return response.access_token; +}; - const response = await fetchData(`http://localhost:5000/users`, "POST", { +const login = async (mail: string, password: string) => { + const response = await fetchData(`http://localhost:5000/auth/login`, "POST", { headers: { "Content-Type": "application/json", }, - body: JSON.stringify(userData), + body: JSON.stringify({ mail, password }), }); - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.first_name).toBe("String must contain at least 2 character(s)"); -}); + return response.token; +}; -test("Create user with short last name", async () => { - userData.last_name = "s"; +// set after creating user +let token1 = ""; +const token2 = await getKeycloakToken(userMail2, userPassword2); - const response = await fetchData(`http://localhost:5000/users`, "POST", { - headers: { - "Content-Type": "application/json", +const updateUser = async (userId: string, token?: string) => { + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "PUT", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), }, - body: JSON.stringify(userData), - }); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.last_name).toBe("String must contain at least 2 character(s)"); -}); + token, + ); -test("Create user with short password", async () => { - userData.password = "1234"; + return response; +}; - const response = await fetchData(`http://localhost:5000/users`, "POST", { - headers: { - "Content-Type": "application/json", +const changePassword = async ( + userId: string, + new_password: string, + repeat_password: string, + token?: string, +) => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/change-password`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + old_password: userData.password, + new_password, + repeat_password, + }), }, - body: JSON.stringify(userData), - }); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.password).toBe("String must contain at least 8 character(s)"); -}); + token, + ); -test("Get user by ID", async () => { - userData.mail = "john_smith@example.com"; - userData.first_name = "John"; - userData.last_name = "Smith"; - userData.password = "12345678"; + return response; +}; +const deleteUser = async (userId: string, token?: string) => { const response = await fetchData( `http://localhost:5000/users/${userId}`, - "GET", + "DELETE", {}, + token, ); - const { status, user } = response; - expect(status).toBe("ok"); - expect(user.id).toBe(userId); - expect(user.first_name).toBe(userData.first_name); - expect(user.last_name).toBe(userData.last_name); - expect(user.mail).toBe(userData.mail); -}); + return response; +}; -test("Get user with incorrect ID", async () => { - const response = await fetchData(`http://localhost:5000/users/🐈`, "GET", {}); +describe("Create user", () => { + test("correct", async () => { + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); - const { status, errors } = response; + const { status, user } = response; - expect(status).toBe("error"); - expect(errors.id).toBe("not found"); -}); + expect(status).toBe("ok"); + expect(user.first_name).toBe(userData.first_name); + expect(user.last_name).toBe(userData.last_name); + expect(user.mail).toBe(userData.mail); -test("Update user by ID", async () => { - userData.profile_picture = "https://example.com/new_john_smith.jpg"; + userId = user.id; + token1 = await login(userData.mail, userData.password); + }); - const response = await fetchData( - `http://localhost:5000/users/${userId}`, - "PUT", - { + test("existing mail", async () => { + userData.mail = "shudghton1@geocities.com"; + + const response = await fetchData(`http://localhost:5000/users`, "POST", { headers: { "Content-Type": "application/json", }, body: JSON.stringify(userData), - }, - ); + }); - const { status } = response; + const { status, errors } = response; - expect(status).toBe("ok"); -}); - -test("Update user with incorrect ID", async () => { - const response = await fetchData(`http://localhost:5000/users/0`, "PUT", { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userData), + expect(status).toBe("error"); + expect(errors.id).toBe("already exists"); }); - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.id).toBe("not found"); -}); - -test("Update user with short first name", async () => { - userData.first_name = "j"; + test("too short first name", async () => { + userData.first_name = "j"; - const response = await fetchData( - `http://localhost:5000/users/${userId}`, - "PUT", - { + const response = await fetchData(`http://localhost:5000/users`, "POST", { headers: { "Content-Type": "application/json", }, body: JSON.stringify(userData), - }, - ); + }); - const { status, errors } = response; + const { status, errors } = response; - expect(status).toBe("error"); - expect(errors.first_name).toBe("String must contain at least 2 character(s)"); -}); + expect(status).toBe("error"); + expect(errors.first_name).toBe( + "String must contain at least 2 character(s)", + ); + }); -test("Update user with short last name", async () => { - userData.last_name = "s"; + test("too short last name", async () => { + userData.last_name = "s"; - const response = await fetchData( - `http://localhost:5000/users/${userId}`, - "PUT", - { + const response = await fetchData(`http://localhost:5000/users`, "POST", { headers: { "Content-Type": "application/json", }, body: JSON.stringify(userData), - }, - ); + }); - const { status, errors } = response; + const { status, errors } = response; - expect(status).toBe("error"); - expect(errors.last_name).toBe("String must contain at least 2 character(s)"); -}); + expect(status).toBe("error"); + expect(errors.last_name).toBe( + "String must contain at least 2 character(s)", + ); + }); -test("Update user with short password", async () => { - await login(userData.mail, userData.password); + test("too short password", async () => { + userData.password = "1234"; - const response = await fetchData( - `http://localhost:5000/users/${userId}/change-password`, - "POST", - { + const response = await fetchData(`http://localhost:5000/users`, "POST", { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - token: token, - old_password: userData.password, - new_password: "1234", - repeat_password: "1234", - }), - }, - ); + body: JSON.stringify(userData), + }); - const { status, errors } = response; + const { status, errors } = response; - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.old_password).toBe("incorrect"); + expect(status).toBe("error"); + expect(errors.password).toBe("String must contain at least 8 character(s)"); + }); }); -test("Update user with same password", async () => { - await login(userData.mail, userData.password); +describe("Get user", () => { + test("incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/🐈`, + "GET", + {}, + ); - const response = await fetchData( - `http://localhost:5000/users/${userId}/change-password`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - token: token, - old_password: userData.password, - new_password: "12345678", - repeat_password: "12345678", - }), - }, - ); + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.id).toBe("not found"); + }); - const { status } = response; + test("all users", async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + const { status, users } = response; - expect(status).toBe("ok"); + expect(status).toBe("ok"); + expect(users.length).toBe(28); + }); + + test("correct", async () => { + userData.mail = "john_smith@example.com"; + userData.first_name = "John"; + userData.last_name = "Smith"; + userData.password = "12345678"; + + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "GET", + {}, + ); + const { status, user } = response; + + expect(status).toBe("ok"); + expect(user.id).toBe(userId); + expect(user.first_name).toBe(userData.first_name); + expect(user.last_name).toBe(userData.last_name); + expect(user.mail).toBe(userData.mail); + }); }); -test("Update user with correct password", async () => { - await login(userData.mail, userData.password); +describe("Update user", () => { + test("without token", async () => { + const { status } = await updateUser(userId); + expect(status).toBe("unauthorized"); + }); - const response = await fetchData( - `http://localhost:5000/users/${userId}/change-password`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - token: token, - old_password: userData.password, - new_password: "123456789", - repeat_password: "123456789", - }), - }, - ); + test("with incorrect token", async () => { + const { status } = await updateUser(userId, token2); + expect(status).toBe("forbidden"); + }); + + // test("incorrect ID", async () => { + // const { status, errors } = await updateUser("0", token1); + + // expect(status).toBe("error"); + // expect(errors.id).toBe("not found"); + // }); + + test("correct", async () => { + userData.profile_picture = "https://example.com/new_john_smith.jpg"; + + const { status } = await updateUser(userId, token1); + expect(status).toBe("ok"); + }); + + test("too short first name", async () => { + userData.first_name = "j"; - const { status } = response; + const { status, errors } = await updateUser(userId, token1); - expect(status).toBe("ok"); + expect(status).toBe("error"); + expect(errors.first_name).toBe( + "String must contain at least 2 character(s)", + ); + }); + + test("too short last name", async () => { + userData.last_name = "s"; + + const { status, errors } = await updateUser(userId, token1); + + expect(status).toBe("error"); + expect(errors.last_name).toBe( + "String must contain at least 2 character(s)", + ); + }); }); -test("Delete user by ID", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}`, - "DELETE", - {}, - ); +describe("Change password", () => { + test("too short", async () => { + const { status, errors } = await changePassword( + userId, + "1234", + "1234", + token1, + ); + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.old_password).toBe("incorrect"); + }); - const { status } = response; + test("identical", async () => { + const { status } = await changePassword( + userId, + "12345678", + "12345678", + token1, + ); + expect(status).toBe("ok"); + }); - expect(status).toBe("ok"); + test("correct", async () => { + const { status } = await changePassword( + userId, + "123456789", + "123456789", + token1, + ); + expect(status).toBe("ok"); + }); }); -test("Delete user with incorrect ID", async () => { - const response = await fetchData( - `http://localhost:5000/users/🐍`, - "DELETE", - {}, - ); +describe("Delete user", () => { + test("without token", async () => { + const { status } = await deleteUser(userId); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await deleteUser(userId, token2); + expect(status).toBe("forbidden"); + }); - const { status, errors } = response; + // test("incorrect ID", async () => { + // const response = await deleteUser("🐍", token1); + // const { status, errors } = response; - expect(status).toBe("error"); - expect(errors.id).toBe("not found"); + // expect(status).toBe("error"); + // expect(errors.id).toBe("not found"); + // }); + + test("correct", async () => { + const { status } = await deleteUser(userId, token1); + expect(status).toBe("ok"); + }); }); diff --git a/backend/test/userFriendRequests.test.ts b/backend/test/userFriendRequests.test.ts index bb538be9..38b2f376 100644 --- a/backend/test/userFriendRequests.test.ts +++ b/backend/test/userFriendRequests.test.ts @@ -1,4 +1,5 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; import User from "../src/models/User.js"; @@ -7,155 +8,56 @@ let maxUsers: number = 1; let userId: string = ""; let userId2: string = ""; +const userMail1 = "bconford2@wikimedia.org"; +const userPassword1 = "heuristic"; + +const userMail2 = "cruckman3@archive.org"; +const userPassword2 = "coreCar0l;"; + const getUsers = async () => { const response = await fetchData(`http://localhost:5000/users`, "GET", {}); - userId = response.users.find( - (user: User) => user.mail === "bconford2@wikimedia.org", - ).id; - userId2 = response.users.find( - (user: User) => user.mail === "cruckman3@archive.org", - ).id; + userId = response.users.find((user: User) => user.mail === userMail1).id; + userId2 = response.users.find((user: User) => user.mail === userMail2).id; }; -await getUsers(); - -test("Check current requests", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, pageCount, friendRequests } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(0); - expect(friendRequests.length).toBe(0); -}); - -test("Check current requests with incorrect ID", async () => { - const response = await fetchData( - `http://localhost:5000/users/0/friend-requests?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.id).toBe("not found"); -}); - -test("Missing page", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests?maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.page).toBe("Invalid input"); -}); - -test("Missing maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests?page=${page}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("Missing page and maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("maxUsers as a text", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests?page=text?page=${page}&maxUsers=text`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Expected number, received nan"); -}); - -test("maxUsers equals 0", async () => { - maxUsers = 0; - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; +const getKeycloakToken = async ( + mail: string, + password: string, +): Promise => { + const urlParams = new URLSearchParams({ + grant_type: "password", + client_id: "mercury-testing", + client_secret: "5mwGU0Efyh3cT2WVX7ffA8UAWEAmrBag", + username: mail, + password: password, + }); - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); -}); - -test("Send invite", async () => { const response = await fetchData( - `http://localhost:5000/users/${userId}/send-friend-request/${userId2}`, + `http://localhost:3000/realms/mercury/protocol/openid-connect/token`, "POST", { headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, - body: JSON.stringify({}), + body: urlParams, }, ); - const { status } = response; - - expect(status).toBe("ok"); -}); - -test("Send invite with incorrect id", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/send-friend-request/0`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); + return response.access_token; +}; - const { status, errors } = response; +await getUsers(); - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId2).toBe("not found"); -}); +const token1 = await getKeycloakToken(userMail1, userPassword1); +const token2 = await getKeycloakToken(userMail2, userPassword2); -test("Decline friend request", async () => { +const sendFriendRequest = async ( + userId1: string, + userId2: string, + token?: string, +) => { const response = await fetchData( - `http://localhost:5000/users/${userId}/decline-friend-request/${userId2}`, + `http://localhost:5000/users/${userId1}/send-friend-request/${userId2}`, "POST", { headers: { @@ -163,16 +65,19 @@ test("Decline friend request", async () => { }, body: JSON.stringify({}), }, + token, ); - const { status } = response; - - expect(status).toBe("ok"); -}); + return response; +}; -test("Decline friend request with incorrect id", async () => { +const declineFriendRequest = async ( + userId1: string, + userId2: string, + token?: string, +) => { const response = await fetchData( - `http://localhost:5000/users/${userId}/decline-friend-request/0`, + `http://localhost:5000/users/${userId1}/decline-friend-request/${userId2}`, "POST", { headers: { @@ -180,18 +85,19 @@ test("Decline friend request with incorrect id", async () => { }, body: JSON.stringify({}), }, + token, ); - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId2).toBe("not found"); -}); + return response; +}; -test("Accept not invited friend request", async () => { +const acceptFriendRequest = async ( + userId1: string, + userId2: string, + token?: string, +) => { const response = await fetchData( - `http://localhost:5000/users/${userId}/accept-friend-request/${userId2}`, + `http://localhost:5000/users/${userId1}/accept-friend-request/${userId2}`, "POST", { headers: { @@ -199,115 +105,266 @@ test("Accept not invited friend request", async () => { }, body: JSON.stringify({}), }, + token, ); - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId1).toBe("not invited"); -}); - -test("Accept invite", async () => { - await fetchData( - `http://localhost:5000/users/${userId}/send-friend-request/${userId2}`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); + return response; +}; +const deleteFriend = async ( + userId1: string, + userId2: string, + token?: string, +) => { const response = await fetchData( - `http://localhost:5000/users/${userId2}/accept-friend-request/${userId}`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); - - const { status } = response; - - expect(status).toBe("ok"); - - const friendsResponse = await fetchData( - `http://localhost:5000/users/${userId2}/friends?page=1&maxUsers=10`, - "GET", + `http://localhost:5000/users/${userId1}/delete-friend/${userId2}`, + "DELETE", {}, + token, ); - const friend = friendsResponse.friends.find( - (user: User) => user.id == userId, - ); + return response; +}; - expect(friend).toBeDefined(); +describe("Get current requests", () => { + test("correct", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, pageCount, friendRequests } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(0); + expect(friendRequests.length).toBe(0); + }); + + test("incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/0/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.id).toBe("not found"); + }); }); -test("Accept invite with incorrect id", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/accept-friend-request/0`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId2).toBe("not found"); +describe("Pagination parameters", () => { + test("missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); + }); + + test("missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); + }); + + test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); + }); }); -test("Decline friend request when not invited", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId2}/decline-friend-request/${userId}`, - "POST", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId1).toBe("not invited"); +describe("Send friend request", () => { + test("without token", async () => { + const { status } = await sendFriendRequest(userId, userId2); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await sendFriendRequest(userId, userId2, token2); + expect(status).toBe("forbidden"); + }); + + test("correct", async () => { + const { status } = await sendFriendRequest(userId, userId2, token1); + expect(status).toBe("ok"); + }); + + test("incorrect id", async () => { + const { status, errors } = await sendFriendRequest(userId, "0", token1); + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); + }); }); -test("Delete friend", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId2}/delete-friend/${userId}`, - "DELETE", - {}, - ); - - const { status } = response; - - expect(status).toBe("ok"); +describe("Decline friend request", () => { + test("without token", async () => { + const { status } = await declineFriendRequest(userId2, userId); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await declineFriendRequest(userId2, userId, token1); + expect(status).toBe("forbidden"); + }); + + test("correct", async () => { + const { status } = await declineFriendRequest(userId2, userId, token2); + expect(status).toBe("ok"); + }); + + test("incorrect id", async () => { + const { status, errors } = await declineFriendRequest(userId2, "0", token2); + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); + }); + + test("not invited", async () => { + const { status, errors } = await declineFriendRequest( + userId2, + userId, + token2, + ); + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId1).toBe("not invited"); + }); }); -test("Delete friend with incorrect id", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/delete-friend/0`, - "DELETE", - {}, - ); - - const { status, errors } = response; +describe("Accept friend request", () => { + test("without token", async () => { + const { status } = await acceptFriendRequest(userId2, userId); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await acceptFriendRequest(userId2, userId, token1); + expect(status).toBe("forbidden"); + }); + + test("incorrect id", async () => { + const response = await acceptFriendRequest(userId, "0", token1); + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); + }); + + test("not invited", async () => { + const { status, errors } = await acceptFriendRequest( + userId2, + userId, + token2, + ); + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId1).toBe("not invited"); + }); + + test("correct", async () => { + await sendFriendRequest(userId, userId2, token1); + + const { status } = await acceptFriendRequest(userId2, userId, token2); + expect(status).toBe("ok"); + + const friendsResponse = await fetchData( + `http://localhost:5000/users/${userId2}/friends?page=1&maxUsers=10`, + "GET", + {}, + token2, + ); + + const friend = friendsResponse.friends.find( + (user: User) => user.id == userId, + ); + + expect(friend).toBeDefined(); + }); +}); - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.userId2).toBe("not found"); +describe("Delete friend", () => { + test("without token", async () => { + const { status } = await deleteFriend(userId2, userId); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await deleteFriend(userId2, userId, token1); + expect(status).toBe("forbidden"); + }); + + test("incorrect id", async () => { + const response = await deleteFriend(userId, "0", token1); + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); + }); + + test("correct", async () => { + const { status } = await deleteFriend(userId2, userId, token2); + expect(status).toBe("ok"); + }); }); diff --git a/backend/test/userFriendSuggestions.test.ts b/backend/test/userFriendSuggestions.test.ts index 3053b5da..25e7b668 100644 --- a/backend/test/userFriendSuggestions.test.ts +++ b/backend/test/userFriendSuggestions.test.ts @@ -1,4 +1,5 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; import User from "../src/models/User.js"; @@ -6,6 +7,12 @@ let userId: number; let page: number = 3; let maxUsers: number = 5; +const userMail1 = "bconford2@wikimedia.org"; +const userPassword1 = "heuristic"; + +const userMail2 = "cruckman3@archive.org"; +const userPassword2 = "coreCar0l;"; + const getFirstUser = async () => { const response = await fetchData(`http://localhost:5000/users`, "GET", {}); userId = response.users.find( @@ -13,120 +20,181 @@ const getFirstUser = async () => { ).id; }; -await getFirstUser(); - -test("Get friend suggestions", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, pageCount, friendSuggestions } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(3); - expect(friendSuggestions).toBeDefined(); - expect(friendSuggestions.length).toBe(3); -}); - -test("Get friend suggestions with incorrect ID", async () => { - const response = await fetchData( - `http://localhost:5000/users/0/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.id).toBe("not found"); -}); - -test("Missing page", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.page).toBe("Invalid input"); -}); - -test("Missing maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("Missing page and maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions`, - "GET", - {}, - ); - - const { status, errors } = response; +const getKeycloakToken = async ( + mail: string, + password: string, +): Promise => { + const urlParams = new URLSearchParams({ + grant_type: "password", + client_id: "mercury-testing", + client_secret: "5mwGU0Efyh3cT2WVX7ffA8UAWEAmrBag", + username: mail, + password: password, + }); - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("maxUsers as a text", async () => { const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?page=text?page=${page}&maxUsers=text`, - "GET", - {}, + `http://localhost:3000/realms/mercury/protocol/openid-connect/token`, + "POST", + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: urlParams, + }, ); - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Expected number, received nan"); -}); - -test("First user", async () => { - page = 1; - maxUsers = 1; - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, pageCount, friendSuggestions } = response; + return response.access_token; +}; - expect(status).toBe("ok"); - expect(pageCount).toBe(13); - expect(friendSuggestions).toBeDefined(); - expect(friendSuggestions.length).toBe(1); +await getFirstUser(); +const token1 = await getKeycloakToken(userMail1, userPassword1); +const token2 = await getKeycloakToken(userMail2, userPassword2); + +describe("Get friend suggestions", () => { + test("without token", async () => { + const { status } = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions` + + `?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const { status } = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions` + + `?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token2, + ); + expect(status).toBe("forbidden"); + }); + + // test("incorrect ID", async () => { + // const response = await fetchData( + // `http://localhost:5000/users/0/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + // "GET", + // {}, + // token1 + // ); + + // const { status, errors } = response; + + // expect(status).toBe("error"); + // expect(errors).toBeDefined(); + // expect(errors.id).toBe("not found"); + // }); + + test("correct", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, pageCount, friendSuggestions } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(3); + expect(friendSuggestions).toBeDefined(); + expect(friendSuggestions.length).toBe(3); + }); + + test("first user", async () => { + page = 1; + maxUsers = 1; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, pageCount, friendSuggestions } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(13); + expect(friendSuggestions).toBeDefined(); + expect(friendSuggestions.length).toBe(1); + }); }); -test("maxUsers equals 0", async () => { - maxUsers = 0; - const response = await fetchData( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); +describe("Pagination parameters", () => { + test("missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); + }); + + test("missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); + }); + + test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); + }); }); diff --git a/backend/test/userFriends.test.ts b/backend/test/userFriends.test.ts index 1f7f1e04..3e801dfe 100644 --- a/backend/test/userFriends.test.ts +++ b/backend/test/userFriends.test.ts @@ -1,4 +1,5 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; import User from "../src/models/User.js"; @@ -6,140 +7,208 @@ let page: number = 1; let maxUsers: number = 10; let userId: string = ""; +const userMail1 = "bconford2@wikimedia.org"; +const userPassword1 = "heuristic"; + +const userMail2 = "cruckman3@archive.org"; +const userPassword2 = "coreCar0l;"; + const getFirstUser = async () => { const response = await fetchData(`http://localhost:5000/users`, "GET", {}); - userId = response.users.find( - (user: User) => user.mail === "bconford2@wikimedia.org", - ).id; + userId = response.users.find((user: User) => user.mail === userMail1).id; }; -await getFirstUser(); - -test("Get friends", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, pageCount, friends } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(1); - expect(friends.length).toBe(9); -}); - -test("Get friends with incorrect ID", async () => { - const response = await fetchData( - `http://localhost:5000/users/0/friends?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.id).toBe("not found"); -}); - -test("Missing page", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); -}); - -test("Page as a text", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=text?maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; +const getKeycloakToken = async ( + mail: string, + password: string, +): Promise => { + const urlParams = new URLSearchParams({ + grant_type: "password", + client_id: "mercury-testing", + client_secret: "5mwGU0Efyh3cT2WVX7ffA8UAWEAmrBag", + username: mail, + password: password, + }); - expect(status).toBe("error"); - expect(errors.page).toBe("Expected number, received nan"); -}); - -test("Missing maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=${page}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("maxUsers as a text", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=text?page=${page}&maxUsers=text`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Expected number, received nan"); -}); - -test("Missing page and maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("First user", async () => { - page = 1; - maxUsers = 1; const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, + `http://localhost:3000/realms/mercury/protocol/openid-connect/token`, + "POST", + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: urlParams, + }, ); - const { status, pageCount, friends } = response; + return response.access_token; +}; - expect(status).toBe("ok"); - expect(pageCount).toBe(9); - expect(friends.length).toBe(1); - expect(friends[0].country).toBe("CN"); - expect(friends[0].mail).toBe("tshillitoe@state.gov"); - expect(friends[0].first_name).toBe("Trever"); - expect(friends[0].last_name).toBe("Shillito"); +await getFirstUser(); +const token1 = await getKeycloakToken(userMail1, userPassword1); +const token2 = await getKeycloakToken(userMail2, userPassword2); + +describe("Get friends", () => { + test("without token", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status } = response; + expect(status).toBe("unauthorized"); + }); + + test("with incorrect token", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token2, + ); + + const { status } = response; + expect(status).toBe("forbidden"); + }); + + // test("incorrect ID", async () => { + // const response = await fetchData( + // `http://localhost:5000/users/0/friends?page=${page}&maxUsers=${maxUsers}`, + // "GET", + // {}, + // token1 + // ); + + // const { status, errors } = response; + + // expect(status).toBe("error"); + // expect(errors).toBeDefined(); + // expect(errors.id).toBe("not found"); + // }); + + test("correct", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, pageCount, friends } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(friends.length).toBe(9); + }); + + test("first user", async () => { + page = 1; + maxUsers = 1; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, pageCount, friends } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(9); + expect(friends.length).toBe(1); + expect(friends[0].country).toBe("CN"); + expect(friends[0].mail).toBe("tshillitoe@state.gov"); + expect(friends[0].first_name).toBe("Trever"); + expect(friends[0].last_name).toBe("Shillito"); + }); }); -test("maxUsers equals 0", async () => { - maxUsers = 0; - const response = await fetchData( - `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); +describe("Pagination parameters", () => { + test("missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + }); + + test("page as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=text?maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Expected number, received nan"); + }); + + test("missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); + }); + + test("missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + token1, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); + }); }); diff --git a/backend/test/userMeetings.test.ts b/backend/test/userMeetings.test.ts deleted file mode 100644 index 88531a17..00000000 --- a/backend/test/userMeetings.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "vitest"; -import { fetchData } from "../src/misc/fetchData.js"; -import User from "../src/models/User.js"; - -let userId: string = ""; - -const getFirstUser = async () => { - const response = await fetchData(`http://localhost:5000/users`, "GET", {}); - userId = response.users.find( - (user: User) => user.mail === "bconford2@wikimedia.org", - ).id; -}; - -await getFirstUser(); - -test("Get users meetings", async () => { - const response = await fetchData( - `http://localhost:5000/users/meetings/${userId}`, - "GET", - {}, - ); - - const { status, meetings } = response; - - expect(status).toBe("ok"); - expect(meetings).toBeDefined(); - expect(meetings.length).toBe(0); -}); - -test("Get users meetings without id", async () => { - const response = await fetchData( - `http://localhost:5000/users/meetings`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.id).toBe("not found"); -}); - -test("Update meeting", async () => { - const response = await fetchData( - `http://localhost:5000/users/meetings/0`, - "PUT", - { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }, - ); - - const { status } = response; - - expect(status).toBe("ok"); -}); diff --git a/backend/test/userSearch.test.ts b/backend/test/userSearch.test.ts index 2fcca022..4c807989 100644 --- a/backend/test/userSearch.test.ts +++ b/backend/test/userSearch.test.ts @@ -1,4 +1,5 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; + import { fetchData } from "../src/misc/fetchData.js"; import User from "../src/models/User.js"; @@ -7,221 +8,227 @@ let maxUsers: number = 32; let query: string = "a"; let country: string = "PL"; -test("Search all users from Poland", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, - "GET", - {}, - ); - - const { status, pageCount, users } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(1); - expect(users).toBeDefined(); - expect(users.length).toBe(4); -}); - -test("Missing country", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&q=`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.country).toBe("Invalid input"); -}); - -test("Missing page", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?maxUsers=${maxUsers}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); -}); - -test("Missing maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("Missing page and maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/search`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.page).toBe("Invalid input"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("maxUsers as a text", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=text?page=${page}&maxUsers=text`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors.maxUsers).toBe("Expected number, received nan"); +describe("Search users", () => { + test("all users from Poland", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(4); + }); + + test("first user from Brazil", async () => { + country = "BR"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(1); + expect(users[0].country).toBe("BR"); + }); + + test("with polish characters", async () => { + query = "Małysz"; + country = "PL"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=${query}`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(4); + + const malysz = users.find((user: User) => user.mail == "adasko@malysz.pl"); + + expect(malysz).toBeDefined(); + }); + + test("not found", async () => { + country = "GR"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(1); + }); }); -test("First user from Brazil", async () => { - country = "BR"; - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, - "GET", - {}, - ); - - const { status, pageCount, users } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(1); - expect(users).toBeDefined(); - expect(users.length).toBe(1); - expect(users[0].country).toBe("BR"); +describe("Search parameters", () => { + test("missing country", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.country).toBe("Invalid input"); + }); + + test("empty query", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, users } = response; + + expect(status).toBe("ok"); + expect(users).toBeDefined(); + }); + + test("missing query", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.q).toBe("Required"); + }); }); -test("Not found users", async () => { - country = "GR"; - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, - "GET", - {}, - ); - - const { status, pageCount, users } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(1); - expect(users).toBeDefined(); - expect(users.length).toBe(1); -}); - -test("Search with polish characters", async () => { - query = "Małysz"; - country = "PL"; - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=${query}`, - "GET", - {}, - ); - - const { status, pageCount, users } = response; - - expect(status).toBe("ok"); - expect(pageCount).toBe(1); - expect(users).toBeDefined(); - expect(users.length).toBe(4); - - const malysz = users.find((user: User) => user.mail == "adasko@malysz.pl"); - - expect(malysz).toBeDefined(); -}); - -test("Repeated page", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&q=`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.page).toBe("Invalid input"); -}); - -test("Repeated maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&maxUsers=${maxUsers}&q=`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("MaxUsers above 32", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=33&country=${country}&q=`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.maxUsers).toBe("Number must be less than or equal to 32"); -}); - -test("Repeated page and maxUsers", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&maxUsers=${maxUsers}&q=`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.page).toBe("Invalid input"); - expect(errors.maxUsers).toBe("Invalid input"); -}); - -test("Empty query", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, - "GET", - {}, - ); - - const { status, users } = response; - - expect(status).toBe("ok"); - expect(users).toBeDefined(); -}); - -test("Missing query", async () => { - const response = await fetchData( - `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}`, - "GET", - {}, - ); - - const { status, errors } = response; - - expect(status).toBe("error"); - expect(errors).toBeDefined(); - expect(errors.q).toBe("Required"); +describe("Pagination parameters", () => { + test("missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + }); + + test("missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); + }); + + test("repeated page", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); + }); + + test("repeated maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&maxUsers=${maxUsers}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); + }); + + test("maxUsers above 32", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=33&country=${country}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be less than or equal to 32"); + }); + + test("repeated page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&maxUsers=${maxUsers}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); + }); }); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index d6fe9904..121aafe6 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -108,5 +108,6 @@ }, "ts-node": { "esm": true - } + }, + "exclude": ["test"] } diff --git a/frontend/src/components/Banner.tsx b/frontend/src/components/Banner.tsx index a8871eeb..2c06d0e0 100644 --- a/frontend/src/components/Banner.tsx +++ b/frontend/src/components/Banner.tsx @@ -1,5 +1,5 @@ -import { Link } from "react-router-dom"; import LogoSVG from "/logo.svg"; +import { Link } from "react-router-dom"; function Banner() { return ( diff --git a/frontend/src/components/ChatBox.tsx b/frontend/src/components/ChatBox.tsx index 15f3a4e9..fd9cfe85 100644 --- a/frontend/src/components/ChatBox.tsx +++ b/frontend/src/components/ChatBox.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; - import { Socket } from "socket.io-client"; -import User from "../models/User"; import notificationSoundUrl from "../assets/notification.mp3"; -import Message, { MessageProps } from "./Message"; +import { useUser } from "../helpers/UserContext"; +import User from "../models/User"; import dataService from "../services/data"; +import Message, { MessageProps } from "./Message"; const notificationSound = new Audio(notificationSoundUrl); @@ -16,6 +16,8 @@ interface ChatBoxProps { } function ChatBox({ user, socket, friendId }: ChatBoxProps) { + const { token } = useUser(); + const messages = useRef([]); const handleScroll = (ref: HTMLDivElement | null) => { @@ -117,6 +119,7 @@ function ChatBox({ user, socket, friendId }: ChatBoxProps) { `/chat/${user.id}/${friendId}`, "GET", {}, + token, ); await addMessages(messageResponse.messages); diff --git a/frontend/src/components/EditDetails.tsx b/frontend/src/components/EditDetails.tsx index bb04e324..1a7ba5c8 100644 --- a/frontend/src/components/EditDetails.tsx +++ b/frontend/src/components/EditDetails.tsx @@ -1,10 +1,11 @@ -import { useForm } from "react-hook-form"; -import User, { FrontendUser } from "../models/User"; import { zodResolver } from "@hookform/resolvers/zod"; -import { userEditDetails } from "../models/RegisterUserSchema"; import { ChangeEvent, useState } from "react"; -import countriesData from "../assets/countries.json"; +import { useForm } from "react-hook-form"; import Select from "react-select"; + +import countriesData from "../assets/countries.json"; +import { userEditDetails } from "../models/RegisterUserSchema"; +import User, { FrontendUser } from "../models/User"; import Popup from "./Popup"; export interface EditDetails { diff --git a/frontend/src/components/EditPassword.tsx b/frontend/src/components/EditPassword.tsx index 48e8bc55..32b76b1b 100644 --- a/frontend/src/components/EditPassword.tsx +++ b/frontend/src/components/EditPassword.tsx @@ -1,10 +1,11 @@ -import { ChangeEvent, FormEvent, useState } from "react"; -import User from "../models/User"; -import { changePasswordSchema } from "../models/RegisterUserSchema"; -import { useNavigate } from "react-router-dom"; import { zodResolver } from "@hookform/resolvers/zod"; +import { ChangeEvent, FormEvent, useState } from "react"; import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; + import { PasswordForm } from "../models/PasswordForm"; +import { changePasswordSchema } from "../models/RegisterUserSchema"; +import User from "../models/User"; export interface EditDetails { provider: string; diff --git a/frontend/src/components/EditPhoto.tsx b/frontend/src/components/EditPhoto.tsx index 35190267..7d5f9343 100644 --- a/frontend/src/components/EditPhoto.tsx +++ b/frontend/src/components/EditPhoto.tsx @@ -1,5 +1,6 @@ -import User from "../models/User"; import { useState } from "react"; + +import User from "../models/User"; import Popup from "./Popup"; export interface EditDetails { diff --git a/frontend/src/components/FoundUser.tsx b/frontend/src/components/FoundUser.tsx index 2206027a..f6a05f21 100644 --- a/frontend/src/components/FoundUser.tsx +++ b/frontend/src/components/FoundUser.tsx @@ -1,7 +1,9 @@ import { useState } from "react"; + +import countriesData from "../assets/countries.json"; +import { useUser } from "../helpers/UserContext"; import User from "../models/User"; import dataService from "../services/data"; -import countriesData from "../assets/countries.json"; interface FoundUserProps { user: User; @@ -13,6 +15,7 @@ interface FoundUserProps { function FoundUser(props: FoundUserProps) { const [requestSent, setRequestSent] = useState(false); const { user, isFriend } = props; + const { token } = useUser(); const countryName = countriesData.find((v) => v.Code == user.country) ?.Country; @@ -44,6 +47,8 @@ function FoundUser(props: FoundUserProps) { await dataService.fetchData( `/users/${props.currentId}/send-friend-request/${props.user.id}`, "POST", + {}, + token, ); setRequestSent(true); } catch (error) { diff --git a/frontend/src/components/Friend.tsx b/frontend/src/components/Friend.tsx index 806aef35..3b4e79ad 100644 --- a/frontend/src/components/Friend.tsx +++ b/frontend/src/components/Friend.tsx @@ -1,12 +1,13 @@ import { + faCommentAlt, faUserMinus, faVideo, - faCommentAlt, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import User from "../models/User"; -import { useNavigate } from "react-router-dom"; import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import User from "../models/User"; import Modal from "./Modal"; export interface FriendProps { diff --git a/frontend/src/components/FriendRequest.tsx b/frontend/src/components/FriendRequest.tsx index e4691ce7..ccee85f9 100644 --- a/frontend/src/components/FriendRequest.tsx +++ b/frontend/src/components/FriendRequest.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; + import User from "../models/User"; import Modal from "./Modal"; diff --git a/frontend/src/components/LoginBox.tsx b/frontend/src/components/LoginBox.tsx index 7d99c0cb..32c44f8b 100644 --- a/frontend/src/components/LoginBox.tsx +++ b/frontend/src/components/LoginBox.tsx @@ -1,6 +1,6 @@ -import { useNavigate } from "react-router-dom"; import { useEffect, useRef, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; + import { useUser } from "../helpers/UserContext"; function LoginBox() { diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 1b8355b2..cb0b91af 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,4 +1,4 @@ -import { motion, AnimatePresence } from "framer-motion"; +import { AnimatePresence, motion } from "framer-motion"; interface ModalInterface { text: string; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index a3a47846..3e052384 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,14 +1,16 @@ -import { Link } from "react-router-dom"; -import LogoSVG from "/logo.svg"; -import { useUser } from "../helpers/UserContext"; -import { useState } from "react"; import { faMagnifyingGlass, + faRightFromBracket, faUser, faUsers, - faRightFromBracket, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import LogoSVG from "/logo.svg"; +import { useState } from "react"; +import { Link } from "react-router-dom"; + +import { useUser } from "../helpers/UserContext"; + ; export interface NavbarProps { diff --git a/frontend/src/components/PaginatorV2.tsx b/frontend/src/components/PaginatorV2.tsx index 54a51d42..fce6dc86 100644 --- a/frontend/src/components/PaginatorV2.tsx +++ b/frontend/src/components/PaginatorV2.tsx @@ -1,11 +1,13 @@ import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEffect, useState } from "react"; + import User from "../models/User"; import dataService from "../services/data"; interface PaginatorProps { endpoint: string; + token?: string; itemsPerPage: number; getItems: (response: any) => User[]; renderItem: (user: User) => React.ReactNode; @@ -14,6 +16,8 @@ interface PaginatorProps { } function PaginatorV2(props: PaginatorProps) { + const { token } = props; + const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [error, setError] = useState(""); @@ -38,7 +42,7 @@ function PaginatorV2(props: PaginatorProps) { console.log(url); await dataService - .fetchData(url, "GET") + .fetchData(url, "GET", {}, token) .then((response) => { const users = props.getItems(response); diff --git a/frontend/src/components/Profile.tsx b/frontend/src/components/Profile.tsx index 271eb0fd..a3bf9efc 100644 --- a/frontend/src/components/Profile.tsx +++ b/frontend/src/components/Profile.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; + +import countriesData from "../assets/countries.json"; import User from "../models/User"; import Modal from "./Modal"; -import countriesData from "../assets/countries.json"; export interface ProfilePageFormProps { user: User; diff --git a/frontend/src/components/RegisterBox.tsx b/frontend/src/components/RegisterBox.tsx index ddcf6cb5..791e7e9d 100644 --- a/frontend/src/components/RegisterBox.tsx +++ b/frontend/src/components/RegisterBox.tsx @@ -1,13 +1,14 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; -import { Link } from "react-router-dom"; import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { userRegisterSchema } from "../models/RegisterUserSchema"; -import { FrontendUser } from "../models/User"; -import * as userPlaceholder from "../assets/user-placeholder.jpg"; -import { useUser } from "../helpers/UserContext"; +import { Link } from "react-router-dom"; import Select from "react-select"; + import countriesData from "../assets/countries.json"; +import * as userPlaceholder from "../assets/user-placeholder.jpg"; +import { useUser } from "../helpers/UserContext"; +import { userRegisterSchema } from "../models/RegisterUserSchema"; +import { FrontendUser } from "../models/User"; function RegisterBox() { const { diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index d3dc77ff..a255e56e 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import Select from "react-select"; + import countriesData from "../assets/countries.json"; import { useUser } from "../helpers/UserContext"; diff --git a/frontend/src/components/Transition.tsx b/frontend/src/components/Transition.tsx index 500e0540..3912f1d3 100644 --- a/frontend/src/components/Transition.tsx +++ b/frontend/src/components/Transition.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from "react"; import { motion, useAnimation } from "framer-motion"; +import React, { useEffect } from "react"; interface TransitionProps { startAnimation: boolean; diff --git a/frontend/src/helpers/KeycloakUserProvider.tsx b/frontend/src/helpers/KeycloakUserProvider.tsx index b85e4d54..8f159ebe 100644 --- a/frontend/src/helpers/KeycloakUserProvider.tsx +++ b/frontend/src/helpers/KeycloakUserProvider.tsx @@ -1,11 +1,12 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; -import dataService from "../services/data"; -import User, { FrontendUser } from "../models/User"; -import { Socket, io } from "socket.io-client"; -import UserContext from "./UserContext"; -import UserState from "../models/UserState"; import Keycloak from "keycloak-js"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { io, Socket } from "socket.io-client"; + +import User, { FrontendUser } from "../models/User"; +import UserState from "../models/UserState"; +import dataService from "../services/data"; +import UserContext from "./UserContext"; function KeycloakUserProvider({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); @@ -24,6 +25,7 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { const fetchFriendsPage = async ( userId: number, page: number, + token: string, ): Promise => { const searchParams = new URLSearchParams({ page: page.toString(), @@ -32,6 +34,8 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { const friendsResponse = await dataService.fetchData( `/users/${userId}/friends?${searchParams}`, "GET", + {}, + token, ); if (friendsResponse.status != "ok") { console.error("Couldn't fetch friends: ", friendsResponse); @@ -41,13 +45,16 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { return friendsResponse.friends; }; - const fetchFriends = async (userId: number): Promise => { + const fetchFriends = async ( + userId: number, + token: string, + ): Promise => { let friends = []; let pageEmpty = false; let page = 1; while (!pageEmpty) { - const friendsPage = await fetchFriendsPage(userId, page); + const friendsPage = await fetchFriendsPage(userId, page, token); if (friendsPage === null) { return null; } @@ -67,7 +74,7 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { return Object.fromEntries(friends.map((f) => [f.id, f])); }; - const updateUserData = async () => { + const updateUserData = async (token: string) => { const keycloak = keycloakRef.current!; const tokenDecoded: any = await keycloak.loadUserInfo(); @@ -84,7 +91,7 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { } const userId = response.user.id; - const newFriendsArray = (await fetchFriends(userId)) || []; + const newFriendsArray = (await fetchFriends(userId, token)) || []; const newFriends = friendsToObject(newFriendsArray); setFriends(newFriends); setUserLoggedIn(response.user); @@ -100,12 +107,12 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { realm: "mercury", clientId: "mercury-client", }); + keycloakRef.current = keycloak; + keycloak.onAuthSuccess = () => { - updateUserData(); setToken(keycloak.token); + updateUserData(keycloak.token!); }; - - keycloakRef.current = keycloak; }); useEffect(() => { @@ -199,10 +206,15 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { if (userState.status != "logged_in") return false; const user = { ...userState.user, ...updateUser }; - const response = await dataService.fetchData(`/users/${user.id}`, "PUT", { - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(user), - }); + const response = await dataService.fetchData( + `/users/${user.id}`, + "PUT", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(user), + }, + token, + ); if (response.status === "ok") { setUserLoggedIn(user); @@ -217,7 +229,12 @@ function KeycloakUserProvider({ children }: { children: React.ReactNode }) { if (userState.status != "logged_in") return true; const user = userState.user!; - const response = await dataService.fetchData(`/users/${user.id}`, "DELETE"); + const response = await dataService.fetchData( + `/users/${user.id}`, + "DELETE", + {}, + token, + ); if (response.status === "ok") { setUserAnonymous(); diff --git a/frontend/src/helpers/MeetingProvider.tsx b/frontend/src/helpers/MeetingProvider.tsx index 78d548f6..83010dea 100644 --- a/frontend/src/helpers/MeetingProvider.tsx +++ b/frontend/src/helpers/MeetingProvider.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useState } from "react"; + import Meeting from "../models/Meeting"; import { useUser } from "./UserContext"; diff --git a/frontend/src/helpers/Protected.tsx b/frontend/src/helpers/Protected.tsx index 09f6d064..55c39d6c 100644 --- a/frontend/src/helpers/Protected.tsx +++ b/frontend/src/helpers/Protected.tsx @@ -1,6 +1,7 @@ import { createContext, useContext, useEffect } from "react"; -import { useUser } from "./UserContext"; + import User from "../models/User"; +import { useUser } from "./UserContext"; export interface ProtectedContextValue { user: User; diff --git a/frontend/src/helpers/RestUserProvider.tsx b/frontend/src/helpers/RestUserProvider.tsx index 6900095f..22a08b1a 100644 --- a/frontend/src/helpers/RestUserProvider.tsx +++ b/frontend/src/helpers/RestUserProvider.tsx @@ -1,12 +1,13 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; -import { isExpired, decodeToken } from "react-jwt"; import Cookies from "js-cookie"; -import dataService from "../services/data"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { decodeToken, isExpired } from "react-jwt"; +import { useNavigate } from "react-router-dom"; +import { io, Socket } from "socket.io-client"; + import User, { FrontendUser } from "../models/User"; -import { Socket, io } from "socket.io-client"; -import UserContext from "./UserContext"; import UserState from "../models/UserState"; -import { useNavigate } from "react-router-dom"; +import dataService from "../services/data"; +import UserContext from "./UserContext"; function RestUserProvider({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); @@ -17,7 +18,9 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { () => (userState.status == "logged_in" ? userState.user : null), [userState], ); - const [token, setToken] = useState(null); + const [decodedToken, setDecodedToken] = useState(); + const [token, setToken] = useState(); + const [socket, setSocket] = useState(null); const [friends, setFriends] = useState>({}); @@ -48,7 +51,8 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { const decodedToken = decodeToken(tokenStr); if (decodedToken && !isExpired(tokenStr)) { - setToken(decodedToken); + setToken(token); + setDecodedToken(decodedToken); return true; } } @@ -103,7 +107,7 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { } sessionStorage.setItem("token", response.token); - setToken(decodeToken(response.token)); + setDecodedToken(decodeToken(response.token) ?? undefined); }; const logout = async () => { @@ -151,10 +155,15 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { if (userState.status != "logged_in") return false; const user = { ...userState.user, ...updateUser }; - const response = await dataService.fetchData(`/users/${user.id}`, "PUT", { - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(user), - }); + const response = await dataService.fetchData( + `/users/${user.id}`, + "PUT", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(user), + }, + token, + ); if (response.status === "ok") { return true; @@ -168,7 +177,12 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { if (userState.status != "logged_in") return true; const user = userState.user!; - const response = await dataService.fetchData(`/users/${user.id}`, "DELETE"); + const response = await dataService.fetchData( + `/users/${user.id}`, + "DELETE", + {}, + token, + ); if (response.status === "ok") { setUserAnonymous(); @@ -190,6 +204,8 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { const friendsResponse = await dataService.fetchData( `/users/${userId}/friends?${searchParams}`, "GET", + {}, + token, ); if (friendsResponse.status != "ok") { console.error("Couldn't fetch friends: ", friendsResponse); @@ -227,8 +243,8 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const handleToken = async () => { - if (token) { - const newUserId = (token as any).userId; + if (decodedToken) { + const newUserId = (decodedToken as any).userId; const response = await dataService.fetchData( `/users/${newUserId}`, "GET", @@ -242,7 +258,7 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { }; handleToken(); - }, [token]); + }, [decodedToken]); if (firstRefresh.current) { firstRefresh.current = false; @@ -259,6 +275,7 @@ function RestUserProvider({ children }: { children: React.ReactNode }) { provider, user, userState, + token, socket, redirectToLogin, login, diff --git a/frontend/src/helpers/UserContext.tsx b/frontend/src/helpers/UserContext.tsx index 058f2c53..2c8eafe4 100644 --- a/frontend/src/helpers/UserContext.tsx +++ b/frontend/src/helpers/UserContext.tsx @@ -1,5 +1,6 @@ import { createContext, useContext } from "react"; import { Socket } from "socket.io-client"; + import User, { FrontendUser } from "../models/User"; import UserState from "../models/UserState"; diff --git a/frontend/src/layout/Reasons.tsx b/frontend/src/layout/Reasons.tsx index ccbf5d0f..212f79fa 100644 --- a/frontend/src/layout/Reasons.tsx +++ b/frontend/src/layout/Reasons.tsx @@ -1,11 +1,11 @@ import { - faShield, faDesktop, - faUsers, faGlobe, + faShield, + faUsers, } from "@fortawesome/free-solid-svg-icons"; - import { motion } from "framer-motion"; + import ReasonCard from "../components/ReasonCard"; export default function Reasons() { diff --git a/frontend/src/layout/Technologies.tsx b/frontend/src/layout/Technologies.tsx index 1e4a610b..967da0d6 100644 --- a/frontend/src/layout/Technologies.tsx +++ b/frontend/src/layout/Technologies.tsx @@ -1,4 +1,5 @@ import { AnimatePresence, motion } from "framer-motion"; + import technologies from "../helpers/TechnologiesContent"; export default function Technologies() { diff --git a/frontend/src/layout/WelcomeMessage.tsx b/frontend/src/layout/WelcomeMessage.tsx index 678c5e75..7b43c47d 100644 --- a/frontend/src/layout/WelcomeMessage.tsx +++ b/frontend/src/layout/WelcomeMessage.tsx @@ -1,7 +1,8 @@ -import { useUser } from "../helpers/UserContext"; import LogoSVG from "/logo.svg"; import { Link } from "react-router-dom"; +import { useUser } from "../helpers/UserContext"; + const scrollToTop = (): void => { window.scrollTo(0, 0); }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 90575beb..a67238ea 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -15,11 +15,11 @@ import VideoCallPage from "./pages/VideoCallPage.tsx"; import "./styles/styles.scss"; -import { store } from "./redux/store.ts"; +import KeycloakUserProvider from "./helpers/KeycloakUserProvider.tsx"; import MeetingProvider from "./helpers/MeetingProvider.tsx"; import Protected from "./helpers/Protected.tsx"; -import KeycloakUserProvider from "./helpers/KeycloakUserProvider.tsx"; import EditDataPage from "./pages/EditDataPage.tsx"; +import { store } from "./redux/store.ts"; const body = document.getElementsByTagName("body")[0]!; body.className = "bg-my-darker text-my-light"; diff --git a/frontend/src/models/RegisterUserSchema.ts b/frontend/src/models/RegisterUserSchema.ts index bc523890..3abc33c4 100644 --- a/frontend/src/models/RegisterUserSchema.ts +++ b/frontend/src/models/RegisterUserSchema.ts @@ -1,6 +1,7 @@ import * as z from "zod"; -import { FrontendUser } from "./User"; + import { PasswordForm } from "./PasswordForm"; +import { FrontendUser } from "./User"; export const userRegisterSchema: z.ZodType> = z.object({ first_name: z diff --git a/frontend/src/pages/EditDataPage.tsx b/frontend/src/pages/EditDataPage.tsx index d364d1c7..390cc088 100644 --- a/frontend/src/pages/EditDataPage.tsx +++ b/frontend/src/pages/EditDataPage.tsx @@ -1,10 +1,11 @@ import { useEffect, useState } from "react"; + +import EditDetails from "../components/EditDetails"; +import EditPassword from "../components/EditPassword"; +import EditPhoto from "../components/EditPhoto"; import Footer from "../components/Footer"; import Navbar from "../components/Navbar"; import Transition from "../components/Transition"; -import EditDetails from "../components/EditDetails"; -import EditPhoto from "../components/EditPhoto"; -import EditPassword from "../components/EditPassword"; import { useUser } from "../helpers/UserContext"; function EditDataPage() { diff --git a/frontend/src/pages/FriendsPage.tsx b/frontend/src/pages/FriendsPage.tsx index a270db6f..044847cc 100644 --- a/frontend/src/pages/FriendsPage.tsx +++ b/frontend/src/pages/FriendsPage.tsx @@ -1,24 +1,25 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { useMeeting } from "../helpers/MeetingProvider"; - import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import Footer from "../components/Footer"; +import FoundUser from "../components/FoundUser"; +import Friend from "../components/Friend"; import FriendRequest from "../components/FriendRequest"; import Navbar from "../components/Navbar"; -import User from "../models/User"; -import dataService from "../services/data"; +import PaginatorV2 from "../components/PaginatorV2"; import Transition from "../components/Transition"; +import { useMeeting } from "../helpers/MeetingProvider"; import { useProtected } from "../helpers/Protected"; -import FoundUser from "../components/FoundUser"; -import Friend from "../components/Friend"; -import PaginatorV2 from "../components/PaginatorV2"; +import { useUser } from "../helpers/UserContext"; +import User from "../models/User"; +import dataService from "../services/data"; function FriendsPage() { const navigate = useNavigate(); const { user } = useProtected(); + const { token } = useUser(); const { meeting, createMeeting, joinMeeting } = useMeeting(); const [friendsRequests, setFriendsRequests] = useState([]); @@ -42,6 +43,7 @@ function FriendsPage() { `/users/${user.id}/friend-requests?page=1&maxUsers=32`, "GET", {}, + token, ); setFriendsRequests(friendsRequestsResponse.friendRequests); } @@ -54,6 +56,8 @@ function FriendsPage() { await dataService.fetchData( `/users/${user.id}/accept-friend-request/${currentId}`, "POST", + {}, + token, ); setRefresh(() => !refresh); @@ -65,6 +69,8 @@ function FriendsPage() { await dataService.fetchData( `/users/${user.id}/decline-friend-request/${friend.id}`, "POST", + {}, + token, ); setRefresh(() => !refresh); @@ -76,6 +82,8 @@ function FriendsPage() { await dataService.fetchData( `/users/${user.id}/delete-friend/${friend.id}`, "DELETE", + {}, + token, ); setRefresh(() => !refresh); @@ -110,6 +118,7 @@ function FriendsPage() { {user && ( =6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", + "integrity": "sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "2.7.x || 3.x", + "prettier": "2 || 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/electron-to-chromium": { + "version": "1.4.772", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.772.tgz", + "integrity": "sha512-jFfEbxR/abTTJA3ci+2ok1NTuOBBtB4jH+UT6PUmRN+DY3WSD4FFRsgoVQ+QNIJ0T7wrXwzsWCI2WKC46b++2A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "node_modules/prettier": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", @@ -24,6 +642,75 @@ "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } } diff --git a/package.json b/package.json index 340b801e..4c83d21b 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ ], "dependencies": { "prettier": "^3.1.0" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1" } }