diff --git a/DevSpaceOApi.json b/DevSpaceOApi.json new file mode 100644 index 0000000..251138e --- /dev/null +++ b/DevSpaceOApi.json @@ -0,0 +1,879 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "DevSpace API", + "description": "API for DevSpace", + "version": "0.1.0" + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "Post": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Post ID" + }, + "title": { + "type": "string", + "description": "Post title" + }, + "content": { + "type": "string", + "description": "Post content" + }, + "createdAt": { + "type": "string", + "description": "Post creation date" + }, + "createdBy": { + "$ref": "#/components/schemas/User" + }, + "likedBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "comments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + } + } + }, + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Comment ID" + }, + "content": { + "type": "string", + "description": "Comment content" + }, + "createdAt": { + "type": "string", + "description": "Comment creation date" + }, + "createdBy": { + "$ref": "#/components/schemas/User" + }, + "likedBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "Notification": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Notification ID" + }, + "message": { + "type": "string", + "description": "Notification message" + }, + "timeStamp": { + "type": "string", + "description": "Notification creation date" + }, + "seen": { + "type": "boolean", + "description": "Notification seen status" + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "User ID" + }, + "firstName": { + "type": "string", + "description": "User first name" + }, + "lastName": { + "type": "string", + "description": "User last name" + } + } + }, + "UserWithRelations": { + "allOf": [ + { + "$ref": "#/components/schemas/User" + }, + { + "type": "object", + "properties": { + "posts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + }, + "comments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + }, + "followed": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "followers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + ] + }, + "UserWithRelationsAndNotifications": { + "allOf": [ + { + "$ref": "#/components/schemas/UserWithRelations" + }, + { + "type": "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + } + } + } + } + ] + } + } + }, + "paths": { + "/auth/signup": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Sign up a new user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "The email of the user" + }, + "password": { + "type": "string", + "description": "The password of the user" + }, + "passwordValidation": { + "type": "string", + "description": "Password validation field" + }, + "firstName": { + "type": "string", + "description": "The first name of the user" + }, + "lastName": { + "type": "string", + "description": "The last name of the user" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully signed up the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid input or user already exists" + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Log in a user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "The email of the user" + }, + "password": { + "type": "string", + "description": "The password of the user" + }, + "longExpiration": { + "type": "boolean", + "description": "Whether to keep the user logged in for a long time" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully logged in the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "token": { + "type": "string", + "description": "The JWT token for the user" + } + } + } + } + } + }, + "400": { + "description": "Missing email or password" + }, + "401": { + "description": "Incorrect email or password" + } + } + } + }, + "/auth/logout": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Authentication" + ], + "summary": "Log out the user", + "responses": { + "200": { + "description": "Successfully logged out the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + } + } + } + } + } + } + } + } + }, + "/posts": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "getAllPosts", + "summary": "Get all posts", + "responses": { + "200": { + "description": "A list of all posts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + } + } + } + } + } + }, + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "createPost", + "summary": "Create a post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Post title" + }, + "content": { + "type": "string", + "description": "Post content" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Title and content are required" + } + } + } + }, + "/posts/{id}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "getPost", + "summary": "Get a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "patch": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "updatePost", + "summary": "Update a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Post title" + }, + "content": { + "type": "string", + "description": "Post content" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or Title and content are required" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "deletePost", + "summary": "Delete a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/posts/followed": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "getFollowedPosts", + "summary": "Get posts of followed users", + "responses": { + "200": { + "description": "A list of posts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + } + } + } + } + } + } + }, + "/posts/{id}/like": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "likePost", + "summary": "Like a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or You have already liked this post" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "unlikePost", + "summary": "Unlike a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or You have not liked this post" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/posts/{id}/comment": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Posts" + ], + "operationId": "commentPost", + "summary": "Comment on a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Comment content" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "A comment", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Comment" + } + } + } + }, + "400": { + "description": "Invalid Request" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Get all users", + "responses": { + "200": { + "description": "A list of all users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Get a single user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the user", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A single user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + } + } + } + }, + "/users/me": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get the currently logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The currently logged in user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelationsAndNotifications" + } + } + } + } + } + } + }, + "/users/{id}/follow": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Follow a user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer", + "description": "The ID of the user to follow" + } + } + ], + "responses": { + "200": { + "description": "Successfully followed the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No user found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Unfollow a user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer", + "description": "The ID of the user to unfollow" + } + } + ], + "responses": { + "200": { + "description": "Successfully unfollowed the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No user found with that ID" + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/client/openapitools.json b/client/openapitools.json new file mode 100644 index 0000000..9841a49 --- /dev/null +++ b/client/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.3.0" + } +} diff --git a/client/package-lock.json b/client/package-lock.json index c7ebb34..4e10a9b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,18 +8,42 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.8", + "@mui/icons-material": "^5.15.8", + "@mui/material": "^5.15.7", + "@reduxjs/toolkit": "^2.1.0", + "axios": "^1.6.7", + "match-sorter": "^6.3.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^9.1.0", + "react-router-dom": "^6.22.0", + "react-scripts": "5.0.1", + "sort-by": "^1.2.0", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "devDependencies": { "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/axios": "^0.14.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.77", "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-scripts": "5.0.1", - "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "@types/react-dom": "^18.2.18" + } + }, + ".api/apis/devspace": { + "name": "@api/devspace", + "version": "0.1.0", + "extraneous": true, + "dependencies": { + "api": "^6.1.1", + "json-schema-to-ts": "^2.8.0-beta.0", + "oas": "^20.10.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -33,7 +57,8 @@ "node_modules/@adobe/css-tools": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==" + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -2288,6 +2313,163 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2381,6 +2563,45 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@fontsource/roboto": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz", + "integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3220,6 +3441,261 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.34", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz", + "integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.7.tgz", + "integrity": "sha512-AuF+Wo2Mp/edaO6vJnWjg+gj4tzEz5ChMZnAQpc22DXpSvM8ddgGcZvM7D7F99pIBoSv8ub+Iz0viL+yuGVmhg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.8", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.8.tgz", + "integrity": "sha512-3Ikivf+BOJ7jT1/71HrbKeicgF9ENM4qo+J1050HMJLtLiJEVXbicnsg2oWJZL+0AsrOMaKnTmx1URBpkctLWg==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz", + "integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.34", + "@mui/core-downloads-tracker": "^5.15.7", + "@mui/system": "^5.15.7", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.7.tgz", + "integrity": "sha512-bcEeeXm7GyQCQvN9dwo8htGv8/6tP05p0i02Z7GXm5EoDPlBcqTNGugsjNLoGq6B0SsdyanjJGw0Jw00o1yAOA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.7", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.7.tgz", + "integrity": "sha512-ixSdslOjK1kzdGcxqj7O3d14By/LPQ7EWknsViQ8RaeT863EAQemS+zvUJDTcOpkfJh6q6gPnYMIb2TJCs9eWA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.7.tgz", + "integrity": "sha512-9alZ4/dLxsTwUOdqakgzxiL5YW6ntqj0CfzWImgWnBMTZhgGcPsbYpBLniNkkk7/jptma4/bykWXHwju/ls/pg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.7", + "@mui/styled-engine": "^5.15.7", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "clsx": "^2.1.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.7.tgz", + "integrity": "sha512-8qhsxQRNV6aEOjjSk6YQIYJxkF5klhj8oG1FEEU4z6HV78TjNqRxMP08QGcdsibEbez+nihAaz6vu83b4XqbAg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3338,6 +3814,55 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz", + "integrity": "sha512-nfJ/b4ZhzUevQ1ZPKjlDL6CMYxO4o7ZL7OSsvSOxzT/EN11LsBDgTqP7aedHtBrFSVoK7oTP1SbMWUwGb30NLg==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", + "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3660,6 +4185,7 @@ "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -3679,6 +4205,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "peer": true, "dependencies": { "color-convert": "^2.0.1" @@ -3694,6 +4221,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "peer": true, "dependencies": { "deep-equal": "^2.0.5" @@ -3703,6 +4231,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "peer": true, "dependencies": { "ansi-styles": "^4.1.0", @@ -3719,6 +4248,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "peer": true, "dependencies": { "color-name": "~1.1.4" @@ -3731,12 +4261,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "peer": true }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "peer": true, "engines": { "node": ">=8" @@ -3746,6 +4278,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "peer": true, "dependencies": { "has-flag": "^4.0.0" @@ -3758,6 +4291,7 @@ "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", + "dev": true, "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", @@ -3779,6 +4313,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3793,6 +4328,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3805,6 +4341,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3815,12 +4352,14 @@ "node_modules/@testing-library/jest-dom/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@testing-library/jest-dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3829,6 +4368,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3840,6 +4380,7 @@ "version": "13.4.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^8.5.0", @@ -3857,6 +4398,7 @@ "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3875,6 +4417,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3889,6 +4432,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -3897,6 +4441,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3912,6 +4457,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3922,12 +4468,14 @@ "node_modules/@testing-library/react/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@testing-library/react/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3936,6 +4484,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3947,6 +4496,7 @@ "version": "13.5.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "dev": true, "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -3977,7 +4527,18 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" + } }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4146,6 +4707,7 @@ "version": "27.5.2", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" @@ -4223,6 +4785,15 @@ "version": "18.2.18", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dependencies": { "@types/react": "*" } @@ -4294,6 +4865,7 @@ "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", + "dev": true, "dependencies": { "@types/jest": "*" } @@ -4303,6 +4875,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -5169,6 +5746,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -5866,6 +6466,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6368,7 +6976,8 @@ "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true }, "node_modules/cssdb": { "version": "7.10.0", @@ -6579,6 +7188,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -6796,7 +7406,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -6806,6 +7417,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -7062,6 +7682,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -8158,6 +8779,11 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8817,6 +9443,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -9170,6 +9809,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "engines": { "node": ">=8" } @@ -9218,6 +9858,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -12101,6 +12742,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "bin": { "lz-string": "bin/bin.js" } @@ -12143,6 +12785,15 @@ "tmpl": "1.0.5" } }, + "node_modules/match-sorter": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz", + "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12247,6 +12898,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "engines": { "node": ">=4" } @@ -12537,6 +13189,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -12556,6 +13209,14 @@ "node": ">= 0.4" } }, + "node_modules/object-path": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz", + "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", @@ -14377,6 +15038,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14665,6 +15331,32 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz", + "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14673,6 +15365,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", + "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", + "dependencies": { + "@remix-run/router": "1.15.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", + "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", + "dependencies": { + "@remix-run/router": "1.15.0", + "react-router": "6.22.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14745,6 +15467,21 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -14792,6 +15529,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -14800,6 +15538,19 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -14912,6 +15663,11 @@ "node": ">= 0.10" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -14945,6 +15701,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", + "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -15592,6 +16353,14 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sort-by": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz", + "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==", + "dependencies": { + "object-path": "0.6.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -15824,6 +16593,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -16018,6 +16788,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "dependencies": { "min-indent": "^1.0.0" }, @@ -16066,6 +16837,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -16872,6 +17648,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index 727bc71..486e14f 100644 --- a/client/package.json +++ b/client/package.json @@ -3,18 +3,32 @@ "version": "0.1.0", "private": true, "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.8", + "@mui/icons-material": "^5.15.8", + "@mui/material": "^5.15.7", + "@reduxjs/toolkit": "^2.1.0", + "axios": "^1.6.7", + "match-sorter": "^6.3.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^9.1.0", + "react-router-dom": "^6.22.0", + "react-scripts": "5.0.1", + "sort-by": "^1.2.0", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "devDependencies": { "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/axios": "^0.14.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.77", "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-scripts": "5.0.1", - "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "@types/react-dom": "^18.2.18" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/client/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index a53698a..0000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/client/src/api/.gitignore b/client/src/api/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/client/src/api/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/client/src/api/.npmignore b/client/src/api/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/client/src/api/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/client/src/api/.openapi-generator-ignore b/client/src/api/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/client/src/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/client/src/api/.openapi-generator/FILES b/client/src/api/.openapi-generator/FILES new file mode 100644 index 0000000..16b445e --- /dev/null +++ b/client/src/api/.openapi-generator/FILES @@ -0,0 +1,9 @@ +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/client/src/api/.openapi-generator/VERSION b/client/src/api/.openapi-generator/VERSION new file mode 100644 index 0000000..8b23b8d --- /dev/null +++ b/client/src/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.3.0 \ No newline at end of file diff --git a/client/src/api/api.ts b/client/src/api/api.ts new file mode 100644 index 0000000..4c5df5b --- /dev/null +++ b/client/src/api/api.ts @@ -0,0 +1,1737 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DevSpace API + * API for DevSpace + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; + +/** + * + * @export + * @interface AuthLoginPost200Response + */ +export interface AuthLoginPost200Response { + /** + * + * @type {string} + * @memberof AuthLoginPost200Response + */ + 'status'?: string; + /** + * The JWT token for the user + * @type {string} + * @memberof AuthLoginPost200Response + */ + 'token'?: string; +} +/** + * + * @export + * @interface AuthLoginPostRequest + */ +export interface AuthLoginPostRequest { + /** + * The email of the user + * @type {string} + * @memberof AuthLoginPostRequest + */ + 'email'?: string; + /** + * The password of the user + * @type {string} + * @memberof AuthLoginPostRequest + */ + 'password'?: string; + /** + * Whether to keep the user logged in for a long time + * @type {boolean} + * @memberof AuthLoginPostRequest + */ + 'longExpiration'?: boolean; +} +/** + * + * @export + * @interface AuthLogoutGet200Response + */ +export interface AuthLogoutGet200Response { + /** + * + * @type {string} + * @memberof AuthLogoutGet200Response + */ + 'status'?: string; +} +/** + * + * @export + * @interface AuthSignupPostRequest + */ +export interface AuthSignupPostRequest { + /** + * The email of the user + * @type {string} + * @memberof AuthSignupPostRequest + */ + 'email'?: string; + /** + * The password of the user + * @type {string} + * @memberof AuthSignupPostRequest + */ + 'password'?: string; + /** + * Password validation field + * @type {string} + * @memberof AuthSignupPostRequest + */ + 'passwordValidation'?: string; + /** + * The first name of the user + * @type {string} + * @memberof AuthSignupPostRequest + */ + 'firstName'?: string; + /** + * The last name of the user + * @type {string} + * @memberof AuthSignupPostRequest + */ + 'lastName'?: string; +} +/** + * + * @export + * @interface Comment + */ +export interface Comment { + /** + * Comment ID + * @type {number} + * @memberof Comment + */ + 'id'?: number; + /** + * Comment content + * @type {string} + * @memberof Comment + */ + 'content'?: string; + /** + * Comment creation date + * @type {string} + * @memberof Comment + */ + 'createdAt'?: string; + /** + * + * @type {User} + * @memberof Comment + */ + 'createdBy'?: User; + /** + * + * @type {Array} + * @memberof Comment + */ + 'likedBy'?: Array; +} +/** + * + * @export + * @interface CommentPostRequest + */ +export interface CommentPostRequest { + /** + * Comment content + * @type {string} + * @memberof CommentPostRequest + */ + 'content'?: string; +} +/** + * + * @export + * @interface CreatePostRequest + */ +export interface CreatePostRequest { + /** + * Post title + * @type {string} + * @memberof CreatePostRequest + */ + 'title'?: string; + /** + * Post content + * @type {string} + * @memberof CreatePostRequest + */ + 'content'?: string; +} +/** + * + * @export + * @interface Notification + */ +export interface Notification { + /** + * Notification ID + * @type {number} + * @memberof Notification + */ + 'id'?: number; + /** + * Notification message + * @type {string} + * @memberof Notification + */ + 'message'?: string; + /** + * Notification creation date + * @type {string} + * @memberof Notification + */ + 'timeStamp'?: string; + /** + * Notification seen status + * @type {boolean} + * @memberof Notification + */ + 'seen'?: boolean; +} +/** + * + * @export + * @interface Post + */ +export interface Post { + /** + * Post ID + * @type {number} + * @memberof Post + */ + 'id'?: number; + /** + * Post title + * @type {string} + * @memberof Post + */ + 'title'?: string; + /** + * Post content + * @type {string} + * @memberof Post + */ + 'content'?: string; + /** + * Post creation date + * @type {string} + * @memberof Post + */ + 'createdAt'?: string; + /** + * + * @type {User} + * @memberof Post + */ + 'createdBy'?: User; + /** + * + * @type {Array} + * @memberof Post + */ + 'likedBy'?: Array; + /** + * + * @type {Array} + * @memberof Post + */ + 'comments'?: Array; +} +/** + * + * @export + * @interface User + */ +export interface User { + /** + * User ID + * @type {number} + * @memberof User + */ + 'id'?: number; + /** + * User first name + * @type {string} + * @memberof User + */ + 'firstName'?: string; + /** + * User last name + * @type {string} + * @memberof User + */ + 'lastName'?: string; +} +/** + * + * @export + * @interface UserWithRelations + */ +export interface UserWithRelations { + /** + * User ID + * @type {number} + * @memberof UserWithRelations + */ + 'id'?: number; + /** + * User first name + * @type {string} + * @memberof UserWithRelations + */ + 'firstName'?: string; + /** + * User last name + * @type {string} + * @memberof UserWithRelations + */ + 'lastName'?: string; + /** + * + * @type {Array} + * @memberof UserWithRelations + */ + 'posts'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelations + */ + 'comments'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelations + */ + 'followed'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelations + */ + 'followers'?: Array; +} +/** + * + * @export + * @interface UserWithRelationsAndNotifications + */ +export interface UserWithRelationsAndNotifications { + /** + * User ID + * @type {number} + * @memberof UserWithRelationsAndNotifications + */ + 'id'?: number; + /** + * User first name + * @type {string} + * @memberof UserWithRelationsAndNotifications + */ + 'firstName'?: string; + /** + * User last name + * @type {string} + * @memberof UserWithRelationsAndNotifications + */ + 'lastName'?: string; + /** + * + * @type {Array} + * @memberof UserWithRelationsAndNotifications + */ + 'posts'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelationsAndNotifications + */ + 'comments'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelationsAndNotifications + */ + 'followed'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelationsAndNotifications + */ + 'followers'?: Array; + /** + * + * @type {Array} + * @memberof UserWithRelationsAndNotifications + */ + 'notifications'?: Array; +} + +/** + * AuthenticationApi - axios parameter creator + * @export + */ +export const AuthenticationApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Log in a user + * @param {AuthLoginPostRequest} authLoginPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authLoginPost: async (authLoginPostRequest: AuthLoginPostRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'authLoginPostRequest' is not null or undefined + assertParamExists('authLoginPost', 'authLoginPostRequest', authLoginPostRequest) + const localVarPath = `/auth/login`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(authLoginPostRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Log out the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authLogoutGet: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/logout`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Sign up a new user + * @param {AuthSignupPostRequest} authSignupPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authSignupPost: async (authSignupPostRequest: AuthSignupPostRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'authSignupPostRequest' is not null or undefined + assertParamExists('authSignupPost', 'authSignupPostRequest', authSignupPostRequest) + const localVarPath = `/auth/signup`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(authSignupPostRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuthenticationApi - functional programming interface + * @export + */ +export const AuthenticationApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuthenticationApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Log in a user + * @param {AuthLoginPostRequest} authLoginPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authLoginPost(authLoginPostRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLoginPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Log out the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authLogoutGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authLogoutGet(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLogoutGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Sign up a new user + * @param {AuthSignupPostRequest} authSignupPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authSignupPost(authSignupPostRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authSignupPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * AuthenticationApi - factory interface + * @export + */ +export const AuthenticationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuthenticationApiFp(configuration) + return { + /** + * + * @summary Log in a user + * @param {AuthLoginPostRequest} authLoginPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: any): AxiosPromise { + return localVarFp.authLoginPost(authLoginPostRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Log out the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authLogoutGet(options?: any): AxiosPromise { + return localVarFp.authLogoutGet(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Sign up a new user + * @param {AuthSignupPostRequest} authSignupPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: any): AxiosPromise { + return localVarFp.authSignupPost(authSignupPostRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AuthenticationApi - object-oriented interface + * @export + * @class AuthenticationApi + * @extends {BaseAPI} + */ +export class AuthenticationApi extends BaseAPI { + /** + * + * @summary Log in a user + * @param {AuthLoginPostRequest} authLoginPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthenticationApi + */ + public authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: RawAxiosRequestConfig) { + return AuthenticationApiFp(this.configuration).authLoginPost(authLoginPostRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Log out the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthenticationApi + */ + public authLogoutGet(options?: RawAxiosRequestConfig) { + return AuthenticationApiFp(this.configuration).authLogoutGet(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Sign up a new user + * @param {AuthSignupPostRequest} authSignupPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthenticationApi + */ + public authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: RawAxiosRequestConfig) { + return AuthenticationApiFp(this.configuration).authSignupPost(authSignupPostRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * PostsApi - axios parameter creator + * @export + */ +export const PostsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Comment on a post by ID + * @param {number} id ID of the post + * @param {CommentPostRequest} commentPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + commentPost: async (id: number, commentPostRequest: CommentPostRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('commentPost', 'id', id) + // verify required parameter 'commentPostRequest' is not null or undefined + assertParamExists('commentPost', 'commentPostRequest', commentPostRequest) + const localVarPath = `/posts/{id}/comment` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(commentPostRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Create a post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPost: async (createPostRequest: CreatePostRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'createPostRequest' is not null or undefined + assertParamExists('createPost', 'createPostRequest', createPostRequest) + const localVarPath = `/posts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createPostRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Delete a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deletePost', 'id', id) + const localVarPath = `/posts/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get all posts + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllPosts: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/posts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get posts of followed users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFollowedPosts: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/posts/followed`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getPost', 'id', id) + const localVarPath = `/posts/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Like a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + likePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('likePost', 'id', id) + const localVarPath = `/posts/{id}/like` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Unlike a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unlikePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('unlikePost', 'id', id) + const localVarPath = `/posts/{id}/like` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Update a post by ID + * @param {number} id ID of the post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePost: async (id: number, createPostRequest: CreatePostRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updatePost', 'id', id) + // verify required parameter 'createPostRequest' is not null or undefined + assertParamExists('updatePost', 'createPostRequest', createPostRequest) + const localVarPath = `/posts/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createPostRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * PostsApi - functional programming interface + * @export + */ +export const PostsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = PostsApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Comment on a post by ID + * @param {number} id ID of the post + * @param {CommentPostRequest} commentPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async commentPost(id: number, commentPostRequest: CommentPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.commentPost(id, commentPostRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.commentPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Create a post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createPost(createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createPost(createPostRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.createPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Delete a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deletePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deletePost(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.deletePost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get all posts + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPosts(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.getAllPosts']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get posts of followed users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getFollowedPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getFollowedPosts(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.getFollowedPosts']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPost(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.getPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Like a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async likePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.likePost(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.likePost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Unlike a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async unlikePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.unlikePost(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.unlikePost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Update a post by ID + * @param {number} id ID of the post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePost(id: number, createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePost(id, createPostRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['PostsApi.updatePost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * PostsApi - factory interface + * @export + */ +export const PostsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = PostsApiFp(configuration) + return { + /** + * + * @summary Comment on a post by ID + * @param {number} id ID of the post + * @param {CommentPostRequest} commentPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + commentPost(id: number, commentPostRequest: CommentPostRequest, options?: any): AxiosPromise { + return localVarFp.commentPost(id, commentPostRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Create a post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPost(createPostRequest: CreatePostRequest, options?: any): AxiosPromise { + return localVarFp.createPost(createPostRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Delete a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePost(id: number, options?: any): AxiosPromise { + return localVarFp.deletePost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get all posts + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllPosts(options?: any): AxiosPromise> { + return localVarFp.getAllPosts(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get posts of followed users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFollowedPosts(options?: any): AxiosPromise> { + return localVarFp.getFollowedPosts(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPost(id: number, options?: any): AxiosPromise { + return localVarFp.getPost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Like a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + likePost(id: number, options?: any): AxiosPromise { + return localVarFp.likePost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Unlike a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unlikePost(id: number, options?: any): AxiosPromise { + return localVarFp.unlikePost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Update a post by ID + * @param {number} id ID of the post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePost(id: number, createPostRequest: CreatePostRequest, options?: any): AxiosPromise { + return localVarFp.updatePost(id, createPostRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * PostsApi - object-oriented interface + * @export + * @class PostsApi + * @extends {BaseAPI} + */ +export class PostsApi extends BaseAPI { + /** + * + * @summary Comment on a post by ID + * @param {number} id ID of the post + * @param {CommentPostRequest} commentPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public commentPost(id: number, commentPostRequest: CommentPostRequest, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).commentPost(id, commentPostRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Create a post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public createPost(createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).createPost(createPostRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Delete a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public deletePost(id: number, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).deletePost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get all posts + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public getAllPosts(options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).getAllPosts(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get posts of followed users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public getFollowedPosts(options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).getFollowedPosts(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public getPost(id: number, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).getPost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Like a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public likePost(id: number, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).likePost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Unlike a post by ID + * @param {number} id ID of the post + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public unlikePost(id: number, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).unlikePost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Update a post by ID + * @param {number} id ID of the post + * @param {CreatePostRequest} createPostRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PostsApi + */ + public updatePost(id: number, createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig) { + return PostsApiFp(this.configuration).updatePost(id, createPostRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * UsersApi - axios parameter creator + * @export + */ +export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersGet: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Unfollow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdFollowDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('usersIdFollowDelete', 'id', id) + const localVarPath = `/users/{id}/follow` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Follow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdFollowPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('usersIdFollowPost', 'id', id) + const localVarPath = `/users/{id}/follow` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get a single user + * @param {number} id ID of the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdGet: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('usersIdGet', 'id', id) + const localVarPath = `/users/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get the currently logged in user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersMeGet: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * UsersApi - functional programming interface + * @export + */ +export const UsersApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Unfollow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowDelete(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Follow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersIdFollowPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowPost(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get a single user + * @param {number} id ID of the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersIdGet(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdGet(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get the currently logged in user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersMeGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersMeGet(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * UsersApi - factory interface + * @export + */ +export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = UsersApiFp(configuration) + return { + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersGet(options?: any): AxiosPromise> { + return localVarFp.usersGet(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Unfollow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdFollowDelete(id: number, options?: any): AxiosPromise { + return localVarFp.usersIdFollowDelete(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Follow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdFollowPost(id: number, options?: any): AxiosPromise { + return localVarFp.usersIdFollowPost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get a single user + * @param {number} id ID of the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersIdGet(id: number, options?: any): AxiosPromise { + return localVarFp.usersIdGet(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get the currently logged in user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersMeGet(options?: any): AxiosPromise { + return localVarFp.usersMeGet(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * UsersApi - object-oriented interface + * @export + * @class UsersApi + * @extends {BaseAPI} + */ +export class UsersApi extends BaseAPI { + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersGet(options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Unfollow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersIdFollowDelete(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Follow a user + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersIdFollowPost(id: number, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersIdFollowPost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get a single user + * @param {number} id ID of the user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersIdGet(id: number, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersIdGet(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get the currently logged in user + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersMeGet(options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersMeGet(options).then((request) => request(this.axios, this.basePath)); + } +} + + + diff --git a/client/src/api/base.ts b/client/src/api/base.ts new file mode 100644 index 0000000..c4c2bc4 --- /dev/null +++ b/client/src/api/base.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DevSpace API + * API for DevSpace + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: RawAxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath ?? basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} + +interface ServerMap { + [key: string]: { + url: string, + description: string, + }[]; +} + +/** + * + * @export + */ +export const operationServerMap: ServerMap = { +} diff --git a/client/src/api/common.ts b/client/src/api/common.ts new file mode 100644 index 0000000..0f54e29 --- /dev/null +++ b/client/src/api/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DevSpace API + * API for DevSpace + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/client/src/api/configuration.ts b/client/src/api/configuration.ts new file mode 100644 index 0000000..02064a6 --- /dev/null +++ b/client/src/api/configuration.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DevSpace API + * API for DevSpace + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + serverIndex?: number; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * override server index + * + * @type {number} + * @memberof Configuration + */ + serverIndex?: number; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.serverIndex = param.serverIndex; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/client/src/api/git_push.sh b/client/src/api/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/client/src/api/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/client/src/api/index.ts b/client/src/api/index.ts new file mode 100644 index 0000000..e3a805a --- /dev/null +++ b/client/src/api/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DevSpace API + * API for DevSpace + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/client/src/app/hooks/useWindowDimensions.ts b/client/src/app/hooks/useWindowDimensions.ts new file mode 100644 index 0000000..6f61af7 --- /dev/null +++ b/client/src/app/hooks/useWindowDimensions.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window; + return { width, height }; +} + +export default function useWindowDimensions() { + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions() + ); + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()); + } + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return windowDimensions; +} diff --git a/client/src/app/loginSlice.ts b/client/src/app/loginSlice.ts new file mode 100644 index 0000000..0d2285e --- /dev/null +++ b/client/src/app/loginSlice.ts @@ -0,0 +1,137 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { Status } from "../util/types"; +import { + AuthLoginPostRequest, + AuthSignupPostRequest, + AuthenticationApi, + Configuration, + UsersApi, +} from "../api"; +import { AppThunk } from "./store"; + +interface loginState { + loggedIn: boolean; + status: Status; + error: string | null; + userInfo: { + firstName: string; + lastName: string; + jwt: string; + }; +} + +const initialState: loginState = { + loggedIn: false, + status: Status.idle, + error: null, + userInfo: { + firstName: "", + lastName: "", + jwt: "", + }, +}; + +export const loginSlice = createSlice({ + name: "login", + initialState, + reducers: { + login: (state, action) => { + state.loggedIn = true; + state.userInfo.jwt = action.payload.jwt; + state.userInfo.firstName = action.payload.firstName; + state.userInfo.lastName = action.payload.lastName; + }, + logoff: (state) => { + state.loggedIn = false; + state.userInfo = initialState.userInfo; + }, + setStatus: (state, action) => { + state.status = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + }, +}); + +const authApi = new AuthenticationApi( + new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + }) +); + +export const postLogin = + (params: AuthLoginPostRequest): AppThunk => + async (dispatch) => { + let response, userResponse; + try { + dispatch(setStatus(Status.loading)); + response = await authApi.authLoginPost(params); + + await addJWT(response.data.token || ""); + + // Get user info + const userApi = new UsersApi( + new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + accessToken: response.data.token, + }) + ); + + userResponse = await userApi.usersMeGet(); + + dispatch( + login({ + jwt: response.data.token, + firstName: userResponse.data.firstName, + lastName: userResponse.data.lastName, + }) + ); + dispatch(setStatus(Status.succeeded)); + } catch (error) { + dispatch(setStatus(Status.failed)); + const errorMessage = "Invalid email or password"; + dispatch(setError(errorMessage)); + } + }; + +export const postSignup = + (params: AuthSignupPostRequest): AppThunk => + async (dispatch) => { + let response; + console.log(params); + try { + dispatch(setStatus(Status.loading)); + response = await authApi.authSignupPost(params); + + dispatch(postLogin({ email: params.email, password: params.password })); + } catch (error) { + dispatch(setStatus(Status.failed)); + const errorMessage = "Change this pls"; + dispatch(setError(errorMessage)); + } + }; + +export const postLogout = (): AppThunk => async (dispatch) => { + localStorage.removeItem("jwt"); + sessionStorage.removeItem("jwt"); + dispatch(logoff()); + await authApi.authLogoutGet(); +}; + +const addJWT = async (token: string) => { + localStorage.setItem("jwt", token); +}; + +export const { login, logoff, setStatus, setError } = loginSlice.actions; + +export default loginSlice.reducer; + +export const selectLoggedIn = (state: { login: loginState }) => + state.login.loggedIn; + +export const selectUserInfo = (state: { login: loginState }) => + state.login.userInfo; + +export const selectErrorMessage = (state: { login: loginState }) => + state.login.error; diff --git a/client/src/app/postSlice.ts b/client/src/app/postSlice.ts new file mode 100644 index 0000000..30ff9c4 --- /dev/null +++ b/client/src/app/postSlice.ts @@ -0,0 +1,88 @@ +import { Store, createSlice } from "@reduxjs/toolkit"; +import { Configuration, Post, PostsApi } from "../api"; +import { Status } from "../util/types"; +import { AppThunk, store } from "./store"; + +interface postSliceInterface { + status: Status; + followedPosts: Post[]; + globalPosts: Post[]; +} + +const initialState: postSliceInterface = { + status: Status.idle, + followedPosts: [], + globalPosts: [], +}; + +export const postSlice = createSlice({ + name: "post", + initialState, + reducers: { + setStatus: (state, action) => { + state.status = action.payload; + }, + setFollowedPosts: (state, action) => { + state.followedPosts = action.payload; + }, + setGlobalPosts: (state, action) => { + state.globalPosts = action.payload; + }, + }, +}); + +export const fetchFollowedPosts = (): AppThunk => async (dispatch: any) => { + const postApi = createApi(store); + + dispatch(setStatus(Status.loading)); + const response = await postApi.getFollowedPosts(); + dispatch(setFollowedPosts(response.data)); + dispatch(setStatus(Status.idle)); +}; + +export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => { + const postApi = createApi(store); + + dispatch(setStatus(Status.loading)); + const response = await postApi.getAllPosts(); + dispatch(setGlobalPosts(response.data)); + dispatch(setStatus(Status.idle)); +}; + +export const likePost = + (postId: number): AppThunk => + async (dispatch: any) => { + const postApi = createApi(store); + + dispatch(setStatus(Status.loading)); + await postApi.likePost(postId); + dispatch(setStatus(Status.idle)); + }; + +export const unLikePost = + (postId: number): AppThunk => + async (dispatch: any) => { + const postApi = createApi(store); + + dispatch(setStatus(Status.loading)); + await postApi.unlikePost(postId); + dispatch(setStatus(Status.idle)); + }; + +export const { setFollowedPosts, setGlobalPosts, setStatus } = + postSlice.actions; + +export default postSlice.reducer; + +export const selectFollowedPosts = (state: any) => state.post.followedPosts; +export const selectAllPosts = (state: any) => state.post.globalPosts; +export const selectStatus = (state: any) => state.post.status; + +function createApi(store: Store) { + return new PostsApi( + new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + accessToken: store.getState().login.userInfo.jwt, + }) + ); +} diff --git a/client/src/app/store.ts b/client/src/app/store.ts new file mode 100644 index 0000000..089d197 --- /dev/null +++ b/client/src/app/store.ts @@ -0,0 +1,22 @@ +import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; +import { useDispatch } from "react-redux"; +import loginReducer from "./loginSlice"; +import postReducer from "./postSlice"; + +export const store = configureStore({ + reducer: { + login: loginReducer, + post: postReducer, + }, +}); + +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType; +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +>; + +export const useAppDispatch: () => AppDispatch = useDispatch; diff --git a/client/src/components/Copyright.tsx b/client/src/components/Copyright.tsx new file mode 100644 index 0000000..825d9f5 --- /dev/null +++ b/client/src/components/Copyright.tsx @@ -0,0 +1,18 @@ +import { Link, Typography } from "@mui/material"; + +export function Copyright(props: any) { + return ( + + {"Copyright © "} + + Tu Trastero Tu Otro Espacio S.L + {" "} + {new Date().getFullYear()}. + + ); +} diff --git a/client/src/components/Drawer.tsx b/client/src/components/Drawer.tsx new file mode 100644 index 0000000..2ce0708 --- /dev/null +++ b/client/src/components/Drawer.tsx @@ -0,0 +1,45 @@ +import {Grid, Hidden, Typography, Drawer, Box} from "@mui/material"; + +export default function ResponsiveDrawer(){ + const drawer = ( +
+ + + Home + + + Search + + + Notifications + + +
+ ); + + return ( + + {/* The implementation can be swapped with js to avoid SEO duplication of links. */} + + + {drawer} + + + + + {drawer} + + + + ); +} \ No newline at end of file diff --git a/client/src/components/StyledComponents.tsx b/client/src/components/StyledComponents.tsx new file mode 100644 index 0000000..74a67c2 --- /dev/null +++ b/client/src/components/StyledComponents.tsx @@ -0,0 +1,5 @@ +import { Divider, styled } from "@mui/material"; + +export const StyledDivider = styled(Divider)({ + marginBottom: "2%", +}); diff --git a/client/src/components/bottomAppBar.tsx b/client/src/components/bottomAppBar.tsx new file mode 100644 index 0000000..a94ff78 --- /dev/null +++ b/client/src/components/bottomAppBar.tsx @@ -0,0 +1,51 @@ +import { BottomNavigation, BottomNavigationAction, Paper } from "@mui/material"; +import { useState } from "react"; +import FeedIcon from "@mui/icons-material/Feed"; +import GlobalIcon from "@mui/icons-material/Public"; +import AddIcon from "@mui/icons-material/Add"; +import SearchIcon from "@mui/icons-material/Search"; +import { useNavigate } from "react-router-dom"; + +export default function BottomAppBar() { + const [value, setValue] = useState(0); + const navigate = useNavigate(); + + const handleClick = (to: string) => () => { + navigate(to); + }; + return ( + + { + setValue(newValue); + }} + > + } + onClick={handleClick("feed")} + /> + } + onClick={handleClick("global")} + /> + } + onClick={handleClick("newpost")} + /> + } + onClick={handleClick("search")} + /> + + + ); +} diff --git a/client/src/components/notificationBell.tsx b/client/src/components/notificationBell.tsx new file mode 100644 index 0000000..064cfb6 --- /dev/null +++ b/client/src/components/notificationBell.tsx @@ -0,0 +1,12 @@ +import NotificationsIcon from "@mui/icons-material/Notifications"; +import { Badge, Tooltip } from "@mui/material"; + +export default function NotificationBell() { + return ( + + + + + + ); +} diff --git a/client/src/components/postListItem.tsx b/client/src/components/postListItem.tsx new file mode 100644 index 0000000..b825d79 --- /dev/null +++ b/client/src/components/postListItem.tsx @@ -0,0 +1,135 @@ +import { + Button, + Card, + CardActionArea, + CardContent, + Grid, + ListItem, + styled, + Typography, +} from "@mui/material"; +import { Post } from "../api"; +import MessageIcon from "@mui/icons-material/Message"; +import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt"; +import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; +import { useNavigate } from "react-router-dom"; +import { likePost, unLikePost } from "../app/postSlice"; +import { useAppDispatch } from "../app/store"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectUserInfo } from "../app/loginSlice"; + +interface PostListItemProps { + post: Post; + postType: "all" | "user"; +} + +export default function PostListItem(props: PostListItemProps) { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const userInfo = useSelector(selectUserInfo); + const [liked, setLiked] = useState(false); + const [numberOfLikes, setNumberOfLikes] = useState(0); + + // On component mount, set the number of likes + // and whether the user has liked the post + useEffect(() => { + setNumberOfLikes(props.post.likedBy?.length || 0); + setLiked(props.post.likedBy?.includes(userInfo) || false); + }, []); + + const handleLike = () => { + if (!liked) { + // Instant feedback to the user + setLiked(true); + setNumberOfLikes(numberOfLikes + 1); + // Dispatch the call to the API + dispatch(likePost(props.post.id as number)); + } + // If the user has already liked the post, unlike it + else { + setLiked(false); + setNumberOfLikes(numberOfLikes - 1); + + dispatch(unLikePost(props.post.id as number)); + } + }; + + const handlePostClick = () => { + navigate(`/post/${props.post.id}`); + }; + const handleUserClick = () => { + navigate(`/user/${props.post.createdBy?.id}`); + }; + + return ( + + + + + + + {props.post.title} + + + {props.post.content} + + + + + + + + + + + + + + ); +} + +const StyledGrid = styled(Grid)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "auto", + marginBottom: "0.8rem", + marginRight: "0.8rem", +}); +const StyledTypography = styled(Typography)({ + padding: "0 0.5rem", +}); diff --git a/client/src/components/sideAppBar.tsx b/client/src/components/sideAppBar.tsx new file mode 100644 index 0000000..9b095cc --- /dev/null +++ b/client/src/components/sideAppBar.tsx @@ -0,0 +1,93 @@ +import { + Divider, + Drawer, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Toolbar, +} from "@mui/material"; + +import FeedIcon from "@mui/icons-material/Feed"; +import GlobalIcon from "@mui/icons-material/Public"; +import AddIcon from "@mui/icons-material/Add"; +import SearchIcon from "@mui/icons-material/Search"; +import { Link, useNavigate } from "react-router-dom"; + +export interface SideAppBarProps { + drawerWidth: number; +} + +export default function SideAppBar(props: SideAppBarProps) { + const { drawerWidth } = props; + const navigate = useNavigate(); + + const handleClick = (to: string) => () => { + navigate(to); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/client/src/components/topAppBar.tsx b/client/src/components/topAppBar.tsx new file mode 100644 index 0000000..4d8ced4 --- /dev/null +++ b/client/src/components/topAppBar.tsx @@ -0,0 +1,112 @@ +import * as React from "react"; +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import Menu from "@mui/material/Menu"; +import Avatar from "@mui/material/Avatar"; +import Tooltip from "@mui/material/Tooltip"; +import MenuItem from "@mui/material/MenuItem"; +import NotificationBell from "./notificationBell"; +import {useSelector} from "react-redux"; +import { postLogout, selectUserInfo } from "../app/loginSlice"; +import { useAppDispatch } from "../app/store"; + +interface TopAppBarProps { + height: number; +} + +function TopAppBar(props: TopAppBarProps) { + const dispatch = useAppDispatch(); + const userInfo = useSelector(selectUserInfo); + const [anchorElUser, setAnchorElUser] = React.useState( + null + ); + + + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleLogout = () => { + dispatch(postLogout()); + setAnchorElUser(null); + }; + + return ( + + + + DevSpace + + + + + + + + + + + + + + {"Profile"} + + + {"Settings"} + + + {"Logout"} + + + + + + ); +} +export default TopAppBar; diff --git a/client/src/error-page.tsx b/client/src/error-page.tsx new file mode 100644 index 0000000..cfa7103 --- /dev/null +++ b/client/src/error-page.tsx @@ -0,0 +1,29 @@ +import {useRouteError} from "react-router-dom"; +import {Box, Typography} from "@mui/material"; + +export default function ErrorPage() { + const error = useRouteError() as any; + console.error(error); + + return ( +
+ + + Whoops! + + + Something went wrong :( + + + + {error.message || error.statusText} + + +
+ ) +} diff --git a/client/src/index.tsx b/client/src/index.tsx index 032464f..1d23b5f 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,15 +1,94 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import "./index.css"; +import reportWebVitals from "./reportWebVitals"; +import "@fontsource/roboto"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Root from "./routes/root"; +import ErrorPage from "./error-page"; +import { createTheme, CssBaseline, ThemeProvider } from "@mui/material"; +import Login from "./routes/Auth/login"; +import Register from "./routes/Auth/register"; +import AuthRoot from "./routes/Auth/authRoot"; +import { Provider } from "react-redux"; +import { store } from "./app/store"; +import PostList from "./routes/postList"; +import Profile from "./routes/profile"; +import Post from "./routes/post"; +import NewPost from "./routes/newPost"; +import Search from "./routes/search"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); + +const defaultTheme = createTheme({ + palette: { + mode: "dark", + }, +}); + +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + children: [ + { + path: "feed", + element: , + }, + { + path: "global", + element: , + }, + { + path: "me", + element: , + }, + { + path: "user/:id", + element: , + }, + { + path: "post/:id", + element: , + }, + { + path: "search", + element: , + }, + { + path: "newpost", + element: , + }, + ], + }, + { + path: "/auth", + element: , + errorElement: , + children: [ + { + path: "login", + element: , + }, + { + path: "register", + element: , + }, + ], + }, +]); + root.render( - + + + + + + ); diff --git a/client/src/routes/Auth/authRoot.tsx b/client/src/routes/Auth/authRoot.tsx new file mode 100644 index 0000000..6a45d52 --- /dev/null +++ b/client/src/routes/Auth/authRoot.tsx @@ -0,0 +1,54 @@ +import { Copyright } from "@mui/icons-material"; +import { Grid, Typography } from "@mui/material"; +import { Outlet, useNavigate } from "react-router-dom"; +import { StyledDivider } from "../../components/StyledComponents"; +import { useSelector } from "react-redux"; +import { selectLoggedIn } from "../../app/loginSlice"; +import { useEffect } from "react"; + +export default function AuthRoot() { + const loggedIn = useSelector(selectLoggedIn); + const navigate = useNavigate(); + + useEffect(() => { + if (loggedIn) { + return navigate("/feed"); + } + }); + + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + direction="column" + spacing={5} + > + + + DevSpace + + + + + + + + + + + ); +} diff --git a/client/src/routes/Auth/login.tsx b/client/src/routes/Auth/login.tsx new file mode 100644 index 0000000..a7ef01a --- /dev/null +++ b/client/src/routes/Auth/login.tsx @@ -0,0 +1,117 @@ +import { + Box, + Button, + Checkbox, + Container, + FormControlLabel, + Grid, + Paper, + TextField, + Typography, + styled, +} from "@mui/material"; +import { StyledDivider } from "../../components/StyledComponents"; +import { Link } from "@mui/material"; +import { useAppDispatch } from "../../app/store"; +import { postLogin, selectErrorMessage } from "../../app/loginSlice"; +import { AuthLoginPostRequest } from "../../api"; +import { useSelector } from "react-redux"; + +export default function Login() { + const dispatch = useAppDispatch(); + const errorMessage = useSelector(selectErrorMessage); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const paramsObject = { + email: data.get("email"), + password: data.get("password"), + longExpiration: data.get("longExpiration"), + }; + + dispatch(postLogin(paramsObject as unknown as AuthLoginPostRequest)); + }; + return ( + <> + + + + + + Email: + + + + Password: + + + + } + label="Remember me" + /> + + + + + + + + + + Don't have an account yet? + + + + + Register now! + + + + + + ); +} + +const StyledGrid = styled(Grid)({ + width: "50%", +}); + +const StyledTypography = styled(Typography)({ + marginBottom: "5%", +}); diff --git a/client/src/routes/Auth/register.tsx b/client/src/routes/Auth/register.tsx new file mode 100644 index 0000000..fb52abd --- /dev/null +++ b/client/src/routes/Auth/register.tsx @@ -0,0 +1,123 @@ +import { + Button, + Container, + Grid, + Link, + Paper, + TextField, + Typography, + styled, +} from "@mui/material"; +import { useAppDispatch } from "../../app/store"; +import { postSignup } from "../../app/loginSlice"; +import { AuthSignupPostRequest } from "../../api"; + +export default function Register() { + const dispatch = useAppDispatch(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const paramsObject = { + email: data.get("email"), + password: data.get("password"), + passwordValidation: data.get("passwordConfirmation"), + firstName: data.get("firstName"), + lastName: data.get("lastName"), + longExpiration: true, + }; + dispatch(postSignup(paramsObject as unknown as AuthSignupPostRequest)); + }; + + return ( + <> + + + + + First Name: + + + + Last Name: + + + + Email: + + + + Password: + + + + Password Confirmation: + + + + + + + + + Already have an account? + + + + + Login + + + + + + ); +} + +const StyledGrid = styled(Grid)({ + width: "50%", +}); +const StyledTypography = styled(Typography)({ + marginBottom: "5%", +}); diff --git a/client/src/routes/newPost.tsx b/client/src/routes/newPost.tsx new file mode 100644 index 0000000..46ac5ce --- /dev/null +++ b/client/src/routes/newPost.tsx @@ -0,0 +1,3 @@ +export default function NewPost() { + return <>New Post; +} diff --git a/client/src/routes/notification.tsx b/client/src/routes/notification.tsx new file mode 100644 index 0000000..9e98dfc --- /dev/null +++ b/client/src/routes/notification.tsx @@ -0,0 +1,3 @@ +export default function Notification() { + return <>Notification; +} diff --git a/client/src/routes/post.tsx b/client/src/routes/post.tsx new file mode 100644 index 0000000..b0a457c --- /dev/null +++ b/client/src/routes/post.tsx @@ -0,0 +1,3 @@ +export default function Post() { + return <>Post; +} diff --git a/client/src/routes/postList.tsx b/client/src/routes/postList.tsx new file mode 100644 index 0000000..40306ee --- /dev/null +++ b/client/src/routes/postList.tsx @@ -0,0 +1,73 @@ +import { useSelector } from "react-redux"; +import { + fetchFollowedPosts, + fetchGlobalPosts, + selectAllPosts, + selectFollowedPosts, +} from "../app/postSlice"; +import { useAppDispatch } from "../app/store"; +import { useEffect } from "react"; +import { Box, List, Typography } from "@mui/material"; +import { Post } from "../api"; +import PostListItem from "../components/postListItem"; + +interface PostListProps { + type: "all" | "user"; +} + +export default function PostList(props: PostListProps) { + const dispatch = useAppDispatch(); + const followedPosts = useSelector(selectFollowedPosts); + const globalPosts = useSelector(selectAllPosts); + + useEffect(() => { + if (props.type === "all") dispatch(fetchGlobalPosts()); + if (props.type === "user") dispatch(fetchFollowedPosts()); + }, []); + + return ( + + {props.type === "all" && ( + + {globalPosts.map((post: Post) => ( + + ))} + + )} + {props.type === "user" && ( + + {followedPosts.map((post: Post) => ( + + ))} + {followedPosts.length === 0 && ( + + + Whops! + + + There is no content here! Try following some users, or check the + global feed + + + )} + + )} + + ); +} diff --git a/client/src/routes/profile.tsx b/client/src/routes/profile.tsx new file mode 100644 index 0000000..ba4f76f --- /dev/null +++ b/client/src/routes/profile.tsx @@ -0,0 +1,12 @@ +import { useParams } from "react-router-dom"; + +export interface ProfileProps { + selfProfile?: boolean; +} + +export default function Profile(props: ProfileProps) { + const { selfProfile } = props; + const userId = useParams<{ userId: string }>().userId; + + return <>Profile; +} diff --git a/client/src/routes/root.tsx b/client/src/routes/root.tsx new file mode 100644 index 0000000..b007b75 --- /dev/null +++ b/client/src/routes/root.tsx @@ -0,0 +1,64 @@ +import { Outlet, useNavigate } from "react-router-dom"; +import { Box } from "@mui/material"; +import { selectLoggedIn } from "../app/loginSlice"; +import { useSelector } from "react-redux"; +import { useEffect } from "react"; +import useWindowDimensions from "../app/hooks/useWindowDimensions"; +import SideAppBar from "../components/sideAppBar"; +import BottomAppBar from "../components/bottomAppBar"; +import TopAppBar from "../components/topAppBar"; + +const drawerWidth = 240; +const topAppBarHeight = 64; + +export default function Root() { + const navigate = useNavigate(); + const loggedIn = useSelector(selectLoggedIn); + const { width } = useWindowDimensions(); + + useEffect(() => { + if (!loggedIn) { + navigate("/auth/login"); + } + }, [loggedIn, navigate]); + + + return ( + <> + + + + + {width > 600 && ( + + + + )} + + 600 ? drawerWidth : 0}px)` }, + marginTop: { xs: `${topAppBarHeight}px`}, + }} + > + + + + {width <= 600 && ( + + + + )} + + + ); +} diff --git a/client/src/routes/search.tsx b/client/src/routes/search.tsx new file mode 100644 index 0000000..935588d --- /dev/null +++ b/client/src/routes/search.tsx @@ -0,0 +1,3 @@ +export default function Search() { + return <>Search; +} diff --git a/client/src/util/types.ts b/client/src/util/types.ts new file mode 100644 index 0000000..571409c --- /dev/null +++ b/client/src/util/types.ts @@ -0,0 +1,6 @@ +export enum Status { + idle = "idle", + loading = "loading", + succeeded = "succeeded", + failed = "failed", +} diff --git a/client/tsconfig.json b/client/tsconfig.json index a273b0c..9d379a3 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +16,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/docker-compose.yml b/docker-compose.yml index b4445b6..31794b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: build: ./client environment: NODE_ENV: development + REACT_APP_BACKEND_URL: http://localhost:3000 ports: - "8080:3000" server: diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 0000000..e73b975 --- /dev/null +++ b/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.2.0" + } +} diff --git a/server/package-lock.json b/server/package-lock.json index 38db4ec..0d9dd72 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,21 +10,69 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.19.1", + "cors": "^2.8.5", "express": "^4.17.2", "jsonwebtoken": "^9.0.2", + "ms": "^2.1.3", "mysql": "^2.14.1", "reflect-metadata": "^0.1.13", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "typeorm": "0.3.20" }, "devDependencies": { "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.5", + "@types/ms": "^0.7.34", "@types/node": "^16.11.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "10.9.1", "typescript": "4.5.2" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -78,6 +126,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -163,6 +216,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -193,6 +255,11 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", @@ -253,6 +320,22 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -397,6 +480,11 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "devOptional": true }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -527,6 +615,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -748,6 +841,14 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -795,6 +896,18 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -884,6 +997,17 @@ "node": ">=0.3.1" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", @@ -939,6 +1063,14 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1423,6 +1555,17 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1463,6 +1606,11 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1473,6 +1621,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -1493,6 +1646,11 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -1770,6 +1928,12 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -2264,6 +2428,94 @@ "node": ">=8" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -2574,6 +2826,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2754,6 +3014,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -2824,6 +3092,34 @@ "engines": { "node": ">=6" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/server/package.json b/server/package.json index f1b34d5..a8124b8 100644 --- a/server/package.json +++ b/server/package.json @@ -5,21 +5,27 @@ "type": "commonjs", "devDependencies": { "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.5", "@types/ms": "^0.7.34", "@types/node": "^16.11.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "10.9.1", "typescript": "4.5.2" }, "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.19.1", + "cors": "^2.8.5", "express": "^4.17.2", "jsonwebtoken": "^9.0.2", "ms": "^2.1.3", "mysql": "^2.14.1", "reflect-metadata": "^0.1.13", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "typeorm": "0.3.20" }, "scripts": { diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index 339a75b..0445823 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -1,53 +1,281 @@ import { AppDataSource } from "../data-source" import { NextFunction, Request, Response } from "express" import { User } from "../entity/User" +import {AppError} from "../util/AppError"; +import {AppRequest} from "../util/AppRequest"; +import {Notification} from "../entity/Notification"; +import {catchAsync} from "../util/catchAsync"; export class UserController { + private userRepository = AppDataSource.getRepository(User); - private userRepository = AppDataSource.getRepository(User) + private notificationRepository = AppDataSource.getRepository(Notification); + /** + * @swagger + * /users: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Get all users + * responses: + * 200: + * description: A list of all users + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/User' + */ + public getAllUsers = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const users = await this.userRepository.find(); - async all(request: Request, response: Response, next: NextFunction) { - return this.userRepository.find() + // remove sensitive fields + users.forEach((user) => { + user.deleteSensitiveFields(); + }); + + res.status(200).send(users); } + ); - async one(request: Request, response: Response, next: NextFunction) { - const id = parseInt(request.params.id) + /** + * @swagger + * /users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Get a single user + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the user + * schema: + * type: integer + * responses: + * 200: + * description: A single user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + */ + public getUser = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const id = req.params.id; + const parsedId = parseInt(id); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); + const user = await this.userRepository.findOne({ + where: { id: parsedId }, + relations: { + followed: true, + followers: true, + posts: true, + comments: true, + }, + }); - const user = await this.userRepository.findOne({ - where: { id } - }) + if (!user) return next(new AppError("No user found with that ID", 404)); - if (!user) { - return "unregistered user" - } - return user + // remove sensitive fields + user.deleteSensitiveFields(); + user.followed.forEach((followedUser) => + followedUser.deleteSensitiveFields() + ); + user.followers.forEach((follower) => follower.deleteSensitiveFields()); + + return res.send(user); } + ); - async save(request: Request, response: Response, next: NextFunction) { - const { firstName, lastName, age } = request.body; + /** + * @swagger + * /users/me: + * get: + * tags: + * - Users + * summary: Get the currently logged in user + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: The currently logged in user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelationsAndNotifications' + */ + public getMe = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const user = await this.userRepository.findOne({ + where: { id: req.user.id }, + relations: { + followed: true, + followers: true, + posts: true, + comments: true, + notifications: true, + }, + }); - const user = Object.assign(new User(), { - firstName, - lastName, - age - }) + user.followed.forEach((followedUser) => + followedUser.deleteSensitiveFields() + ); + user.followers.forEach((follower) => follower.deleteSensitiveFields()); - return this.userRepository.save(user) + return res.status(200).send(user); } + ); - async remove(request: Request, response: Response, next: NextFunction) { - const id = parseInt(request.params.id) + /** + * @swagger + * /users/{id}/follow: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Follow a user + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the user to follow + * responses: + * 200: + * description: Successfully followed the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + * 400: + * description: Invalid ID + * 404: + * description: No user found with that ID + */ + public followUser = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const userToFollowId = req.params.id; + const parsedId = parseInt(userToFollowId); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); - let userToRemove = await this.userRepository.findOneBy({ id }) + const user = req.user; + const userToFollow = await this.userRepository.findOne({ + where: { id: parsedId }, + relations: { followed: true, followers: true, notifications: true }, + }); - if (!userToRemove) { - return "this user not exist" - } + if (!userToFollow) + return next(new AppError("No user found with that ID", 404)); - await this.userRepository.remove(userToRemove) + // Check if user is already following + if ( + user.followed.some( + (followedUser) => followedUser.id === userToFollow.id + ) + ) { + return next(new AppError("You are already following this user", 400)); + } + // Follow the user + user.followed.push(userToFollow); + await this.userRepository.save(user); + // Add the requesting user to the followers of the user being followed + userToFollow.followers.push(user); + // Create a notification for the user being followed + const followNotification = Object.assign(new Notification(), { + seen: false, + message: `${user.firstName} is now following you`, + timeStamp: new Date(), + }); + userToFollow.notifications.push( + await this.notificationRepository.save(followNotification) + ); + await this.userRepository.save(userToFollow); - return "user has been removed" + return res.status(200).send({ + status: "success", + message: `You are now following ${userToFollow.firstName}`, + }); } + ); + /** + * @swagger + * /users/{id}/follow: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Unfollow a user + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the user to unfollow + * responses: + * 200: + * description: Successfully unfollowed the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + * 400: + * description: Invalid ID + * 404: + * description: No user found with that ID + */ + public unfollowUser = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const userToUnfollowId = req.params.id; + const parsedId = parseInt(userToUnfollowId); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); + + const user = req.user; + const userToUnfollow = await this.userRepository.findOne({ + where: { id: parsedId }, + relations: { followed: true, followers: true }, + }); + + if (!userToUnfollow) + return next(new AppError("No user found with that ID", 404)); + // Check if user is following + if ( + !user.followed.some( + (followedUser) => followedUser.id === userToUnfollow.id + ) + ) { + return next(new AppError("You are not following this user", 400)); + } + // Unfollow the user + user.followed = user.followed.filter( + (followedUser) => followedUser.id !== userToUnfollow.id + ); + await this.userRepository.save(user); + + userToUnfollow.followers = userToUnfollow.followers.filter( + (follower) => follower.id !== user.id + ); + await this.userRepository.save(userToUnfollow); + + return res.status(200).send({ + status: "success", + message: `You are no longer following ${userToUnfollow.firstName}`, + }); + } + ); } \ No newline at end of file diff --git a/server/src/controller/authController.ts b/server/src/controller/authController.ts index 50af6dd..f01ead1 100644 --- a/server/src/controller/authController.ts +++ b/server/src/controller/authController.ts @@ -38,9 +38,48 @@ export class AuthController { } - public handleSignUp = catchAsync(async (req, res, next) => { - const {email, password, passwordValidation, firstName, lastName} = req.body - // Body Validation + /** + * @swagger + * /auth/signup: + * post: + * tags: + * - Authentication + * summary: Sign up a new user + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: The email of the user + * password: + * type: string + * description: The password of the user + * passwordValidation: + * type: string + * description: Password validation field + * firstName: + * type: string + * description: The first name of the user + * lastName: + * type: string + * description: The last name of the user + * responses: + * 200: + * description: Successfully signed up the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 400: + * description: Invalid input or user already exists + */ + public handleSignUp = catchAsync(async (req, res, next) => { + const {email, password, passwordValidation, firstName, lastName} = req.body + // Body Validation if (password != passwordValidation) { return next(new AppError('Passwords do not match', 400)) @@ -64,11 +103,53 @@ export class AuthController { this.sendToken(await this.userRepository.save(newUser), res) }) - public handleLogin = catchAsync(async (req, res, next) => { - const {email, password, longExpiration} = req.body - if (!email || !password) { - return next(new AppError('Please provide email and password', 400)) - } + /** + * @swagger + * /auth/login: + * post: + * tags: + * - Authentication + * summary: Log in a user + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: The email of the user + * password: + * type: string + * description: The password of the user + * longExpiration: + * type: boolean + * description: Whether to keep the user logged in for a long time + * responses: + * 200: + * description: Successfully logged in the user + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * token: + * type: string + * description: The JWT token for the user + * 400: + * description: Missing email or password + * 401: + * description: Incorrect email or password + */ + public handleLogin = catchAsync(async (req, res, next) => { + const { email, password, longExpiration } = req.body; + if (!email || !password) { + return next(new AppError("Please provide email and password", 400)); + } const user = await this.userRepository.findOne({where: {email}}) if (!user || !(await user.comparePassword(password))) { @@ -79,14 +160,35 @@ export class AuthController { this.sendToken(user, res, {long: longExpiration}) }) - public handleLogout = catchAsync(async (req: AppRequest, res, next) => { - // Set the jwt cookie to a dummy value and set the expiration to a date in the past - res.cookie('jwt', 'loggedout', { - expires: new Date(Date.now() - 10000), - httpOnly: true - }) - res.status(200).json({status: 'success'}) - }) + /** + * @swagger + * /auth/logout: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Authentication + * summary: Log out the user + * responses: + * 200: + * description: Successfully logged out the user + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + */ + public handleLogout = catchAsync(async (req: AppRequest, res, next) => { + // Set the jwt cookie to a dummy value and set the expiration to a date in the past + res.cookie("jwt", "loggedout", { + expires: new Date(Date.now() - 10000), + httpOnly: true, + }); + res.status(200).json({ status: "success" }); + }); public protect = catchAsync(async (req: AppRequest, res, next) => { let token: string | undefined; @@ -106,7 +208,10 @@ export class AuthController { // Verify the token const decoded = jwt.verify(token, this.jwt_secret) as JwtPayload // Check if the user still exists - const candidateUser = await this.userRepository.findOne({where: {id: decoded.id}}) + const candidateUser = await this.userRepository.findOne({ + where: {id: decoded.id}, + relations: {followed: true, followers: true, notifications: true} + }) if(!candidateUser){ return next(new AppError('The user belonging to this token no longer exists', 401)) } diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts new file mode 100644 index 0000000..b5293ed --- /dev/null +++ b/server/src/controller/postController.ts @@ -0,0 +1,515 @@ +import {AppDataSource} from "../data-source"; +import {User} from "../entity/User"; +import {Post} from "../entity/Post"; +import {Comment} from "../entity/Comment"; +import {Notification} from "../entity/Notification"; +import {catchAsync} from "../util/catchAsync"; +import {AppError} from "../util/AppError"; +import {AppRequest} from "../util/AppRequest"; + +export class PostController { + private postRepository = AppDataSource.getRepository(Post) + + private commentRepository = AppDataSource.getRepository(Comment) + + private notificationRepository = AppDataSource.getRepository(Notification) + + private userRepository = AppDataSource.getRepository(User) + + /** + * @swagger + * /posts: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getAllPosts + * summary: Get all posts + * responses: + * 200: + * description: A list of all posts + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Post' + */ + public getAllPosts = catchAsync(async (_req, res, _next) => { + const posts = await this.postRepository.find({ + relations: { createdBy: true, likedBy: true, comments: {createdBy: true} }, + }); + + // Remove sensitive fields + posts.forEach(post => { + post.deleteSensitiveFields() + }) + + res.status(200).send(posts); + }); + + /** + * @swagger + * /posts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getPost + * summary: Get a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ + public getPost = catchAsync(async (req, res, next) => { + const { post, errorMessage } = await this.validateRequestAndGetEntities( + req + ); + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.send(post); + }); + + /** + * @swagger + * /posts/followed: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getFollowedPosts + * summary: Get posts of followed users + * responses: + * 200: + * description: A list of posts + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Post' + */ + + public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => { + const user = await this.userRepository.findOne({ + where: { id: req.user.id }, + relations: { + followed: true, + posts: { likedBy: true, comments: true }, + }, + }); + + const followedPosts = user.followed.map(followedUser => followedUser.posts).flat() + + // Remove sensitive fields + followedPosts.forEach(post => { + post.deleteSensitiveFields() + }) + + res.send(followedPosts); + }); + + /** + * @swagger + * /posts: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: createPost + * summary: Create a post + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 201: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Title and content are required + */ + + public createPost = catchAsync(async (req : AppRequest, res, next) => { + const user = await this.userRepository.findOne({where: {id: req.user.id}}) + const {title, content} = req.body + + if(!title || !content) return next(new AppError('Title and content are required', 400)) + + + const newPost = Object.assign(new Post(), { + title, + content, + createdBy: user, + createdAt: new Date()}) + + const post = await this.postRepository.save(newPost) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(201).send(post); + }); + + /** + * @swagger + * /posts/{id}: + * patch: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: updatePost + * summary: Update a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or Title and content are required + * 404: + * description: No post found with that ID + */ + + public updatePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + const {title, content} = req.body + + if(!title || !content) return next(new AppError('Title and content are required', 400)) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + if(post.createdBy.id !== user.id){ + return next(new AppError('You are not authorized to update this post', 403)) + } + + post.title = title + post.content = content + await this.postRepository.save(post) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: deletePost + * summary: Delete a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 204: + * description: No content + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ + + public deletePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) + + if(post.createdBy.id !== user.id){ + return next(new AppError('You are not authorized to delete this post', 403)) + } + + await this.postRepository.remove(post); + res.status(204).send(); + }); + + /** + * @swagger + * /posts/{id}/like: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: likePost + * summary: Like a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have already liked this post + * 404: + * description: No post found with that ID + */ + + public likePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + // Check if user has already liked the post + if(post.likedBy.some(likedUser => likedUser.id === user.id)){ + return next(new AppError('You have already liked this post', 400)) + } + + post.likedBy.push(user) + await this.postRepository.save(post) + + // Send notification to post creator + const newNotification = Object.assign(new Notification(),{ + message: `${user.firstName} ${user.lastName} liked your post`, + belongsTo: post.createdBy, + timeStamp: new Date() + }) + const notification = await this.notificationRepository.save(newNotification) + post.createdBy.notifications.push(notification) + await this.userRepository.save(post.createdBy) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/like: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: unlikePost + * summary: Unlike a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have not liked this post + * 404: + * description: No post found with that ID + */ + + public unlikePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) + + // Check if user likes the post + if(!post.likedBy.some(likedUser => likedUser.id === user.id)){ + return next(new AppError('You have not liked this post', 400)) + } + + post.likedBy = post.likedBy.filter(likedUser => likedUser.id !== user.id) + await this.postRepository.save(post) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/comment: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: commentPost + * summary: Comment on a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * description: Comment content + * responses: + * 201: + * description: A comment + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Comment' + * 400: + * description: Invalid Request + * 404: + * description: No post found with that ID + */ + + public commentPost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') { + return next(new AppError('No post found with that ID', 404)) + } + + const {content} = req.body + const newComment = Object.assign(new Comment(), { + content, + createdBy: user, + post, + createdAt: new Date() + }) + + const comment = await this.commentRepository.save(newComment) + + // Send notification to post creator + const newNotification = Object.assign(new Notification(),{ + message: `${user.firstName} ${user.lastName} commented on your post`, + belongsTo: post.createdBy, + timeStamp: new Date() + }) + await this.notificationRepository.save(newNotification) + + // Remove sensitive fields + comment.createdBy.deleteSensitiveFields() + post.deleteSensitiveFields() + + res.status(201).send(comment) + }) + + + private validateRequestAndGetEntities = async (req : AppRequest,) : Promise => { + const postId = req.params.id + const parsedId = parseInt(postId) + let errorMessage: 'Invalid ID' | 'No post found with that ID' + + // Check if ID is a number + if(isNaN(parsedId)) errorMessage = 'Invalid ID' + + const user = req.user + const post = await this.postRepository.findOne({ + where: {id: parsedId}, + relations: { + likedBy: true, + comments: { createdBy: true }, + createdBy: {notifications: true} + } + }) + + // Check if post exists + if(!post) errorMessage = 'No post found with that ID' + + return {user, post, errorMessage} + + } +} +interface validatedEntities { + user: User + post: Post + errorMessage?: 'Invalid ID' | 'No post found with that ID' +} diff --git a/server/src/data-source.ts b/server/src/data-source.ts index 879c428..6d37e96 100644 --- a/server/src/data-source.ts +++ b/server/src/data-source.ts @@ -3,6 +3,7 @@ import { DataSource } from "typeorm" import { User } from "./entity/User" import {Comment} from "./entity/Comment"; import {Post} from "./entity/Post"; +import {Notification} from "./entity/Notification"; export const AppDataSource = new DataSource({ type: "mysql", @@ -13,7 +14,7 @@ export const AppDataSource = new DataSource({ database: process.env.MYSQL_DATABASE, synchronize: true, logging: false, - entities: [User, Comment, Post], + entities: [User, Comment, Post, Notification], migrations: [], subscribers: [], }) diff --git a/server/src/entity/Comment.ts b/server/src/entity/Comment.ts index c7bd23f..2e6ccf0 100644 --- a/server/src/entity/Comment.ts +++ b/server/src/entity/Comment.ts @@ -1,5 +1,6 @@ import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; import {User} from "./User"; +import {Post} from "./Post"; @Entity() export class Comment { @@ -12,9 +13,12 @@ export class Comment { @Column() createdAt: Date - @ManyToOne(() => User, user => user.posts) + @ManyToOne(() => User, user => user.comments) createdBy: User + @ManyToOne(() => Post, post => post.comments) + post: Post + @ManyToMany(()=> User) @JoinTable() likedBy: User[] diff --git a/server/src/entity/Notification.ts b/server/src/entity/Notification.ts new file mode 100644 index 0000000..8e06d47 --- /dev/null +++ b/server/src/entity/Notification.ts @@ -0,0 +1,21 @@ +import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; +import {User} from "./User"; + +@Entity() +export class Notification { + + @PrimaryGeneratedColumn() + id: number + + @Column() + message: string + + @Column() + timeStamp: Date + + @Column({default: false}) + seen: boolean + + @ManyToOne(type => User, user => user.notifications) + belongsTo: User +} \ No newline at end of file diff --git a/server/src/entity/Post.ts b/server/src/entity/Post.ts index f9a9847..9b7eb14 100644 --- a/server/src/entity/Post.ts +++ b/server/src/entity/Post.ts @@ -1,5 +1,6 @@ -import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; +import {Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn} from "typeorm"; import {User} from "./User"; +import {Comment} from "./Comment"; @Entity() export class Post { @@ -18,8 +19,22 @@ export class Post { @ManyToOne(() => User, user => user.posts) createdBy: User + @OneToMany(() => Comment, comment => comment.post) + comments: Comment[] + @ManyToMany(()=> User) @JoinTable() likedBy: User[] + public deleteSensitiveFields(){ + this.createdBy.deleteSensitiveFields() + if(this.likedBy.length > 0){ + this.likedBy.forEach(user => user.deleteSensitiveFields()) + } + if(this.comments.length > 0){ + this.comments.forEach(comment => comment.createdBy.deleteSensitiveFields()) + } + + } + } \ No newline at end of file diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index 2927f71..d2b9705 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -2,6 +2,7 @@ import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, OneToMany, JoinTable import * as bcrypt from "bcrypt" import {Post} from "./Post"; import {Comment} from "./Comment"; +import {Notification} from "./Notification"; @Entity() export class User { @@ -25,12 +26,19 @@ export class User { @JoinTable() followed: User[] + @ManyToMany(type => User) + @JoinTable() + followers: User[] + @OneToMany(type => Post, post=> post.createdBy) posts: Post[] @OneToMany(type => Comment, comment=> comment.createdBy) comments: Comment[] + @OneToMany(type => Notification, notification => notification.belongsTo) + notifications: Notification[] + static async hashPassword(password: string){ return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS)) @@ -39,4 +47,12 @@ export class User { async comparePassword(password: string){ return await bcrypt.compare(password, this.password) } + + public deleteSensitiveFields(){ + delete this.password + delete this.email + delete this.notifications + delete this.followed + delete this.followers + } } diff --git a/server/src/index.ts b/server/src/index.ts index 71bccab..d7d355f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,24 +3,40 @@ import * as bodyParser from "body-parser" import { AppDataSource } from "./data-source" import {errorHandler} from "./controller/errorController"; import {AuthRoutes} from "./routes/authRoutes"; +import {UserRoutes} from "./routes/userRoutes"; +import {AuthController} from "./controller/authController"; +import {PostRoutes} from "./routes/postRoutes"; +import {swaggerRouter} from "./routes/swaggerRoutes"; +import * as cors from "cors"; -AppDataSource.initialize().then(async () => { - +AppDataSource.initialize() + .then(async () => { // create express app - const app = express() - app.use(bodyParser.json()) + const app = express(); + app.use(bodyParser.json()); + app.use(cors()); // register express routes from defined application routes // Auth Routes - app.use('/auth', AuthRoutes) + app.use("/auth", AuthRoutes); + + // Swagger Routes + app.use("/docs", swaggerRouter); + + // All routes after this one require authentication + const authController = new AuthController(); + app.use(authController.protect); + + app.use("/users", UserRoutes); + app.use("/posts", PostRoutes); // setup express app here - app.use(errorHandler) + app.use(errorHandler); // start express server - app.listen(3000) + app.listen(3000); - - - console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results") - -}).catch(error => console.log(error)) + console.log( + "Express server has started on port 3000. Open http://localhost:3000/users to see results" + ); + }) + .catch((error) => console.log(error)); diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts index 7762739..2731ad7 100644 --- a/server/src/routes/authRoutes.ts +++ b/server/src/routes/authRoutes.ts @@ -7,6 +7,7 @@ export const AuthRoutes = Router(); AuthRoutes.route("/signup").post(authController.handleSignUp) AuthRoutes.route("/login").post(authController.handleLogin) +AuthRoutes.route("/logout").get(authController.handleLogout) diff --git a/server/src/routes/postRoutes.ts b/server/src/routes/postRoutes.ts new file mode 100644 index 0000000..47ed5d0 --- /dev/null +++ b/server/src/routes/postRoutes.ts @@ -0,0 +1,19 @@ +import {PostController} from "../controller/postController"; +import {Router} from "express"; + +const postController = new PostController() + +export const PostRoutes = Router(); + +PostRoutes.route("/") + .get(postController.getAllPosts) + .post(postController.createPost) +PostRoutes.route("/followed").get(postController.getFollowedPosts) +PostRoutes.route("/:id") + .get(postController.getPost) + .patch(postController.updatePost) + .delete(postController.deletePost) +PostRoutes.route("/:id/like") + .post(postController.likePost) + .delete(postController.unlikePost) +PostRoutes.route("/:id/comment").post(postController.commentPost) \ No newline at end of file diff --git a/server/src/routes/swaggerRoutes.ts b/server/src/routes/swaggerRoutes.ts new file mode 100644 index 0000000..152055e --- /dev/null +++ b/server/src/routes/swaggerRoutes.ts @@ -0,0 +1,194 @@ +import * as swaggerJSdoc from "swagger-jsdoc"; +import {Router} from "express"; +import * as swaggerUi from "swagger-ui-express"; +import { catchAsync } from "../util/catchAsync"; + +const swaggerOptions = { + swaggerDefinition: { + openapi: "3.0.0", + info: { + title: "DevSpace API", + description: "API for DevSpace", + version: "0.1.0", + }, + + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + schemas: { + Post: { + type: "object", + properties: { + id: { + type: "integer", + description: "Post ID", + }, + title: { + type: "string", + description: "Post title", + }, + content: { + type: "string", + description: "Post content", + }, + createdAt: { + type: "string", + description: "Post creation date", + }, + createdBy: { + $ref: "#/components/schemas/User", + }, + likedBy: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + comments: { + type: "array", + items: { + $ref: "#/components/schemas/Comment", + }, + }, + }, + }, + Comment: { + type: "object", + properties: { + id: { + type: "integer", + description: "Comment ID", + }, + content: { + type: "string", + description: "Comment content", + }, + createdAt: { + type: "string", + description: "Comment creation date", + }, + createdBy: { + $ref: "#/components/schemas/User", + }, + likedBy: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + Notification: { + type: "object", + properties: { + id: { + type: "integer", + description: "Notification ID", + }, + message: { + type: "string", + description: "Notification message", + }, + timeStamp: { + type: "string", + description: "Notification creation date", + }, + seen: { + type: "boolean", + description: "Notification seen status", + }, + }, + }, + User: { + type: "object", + properties: { + id: { + type: "integer", + description: "User ID", + }, + firstName: { + type: "string", + description: "User first name", + }, + lastName: { + type: "string", + description: "User last name", + }, + }, + }, + UserWithRelations: { + allOf: [ + { + $ref: "#/components/schemas/User", + }, + { + type: "object", + properties: { + posts: { + type: "array", + items: { + $ref: "#/components/schemas/Post", + }, + }, + comments: { + type: "array", + items: { + $ref: "#/components/schemas/Comment", // Assuming you have a Comment schema + }, + }, + followed: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + followers: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + ], + }, + UserWithRelationsAndNotifications: { + allOf: [ + { $ref: "#/components/schemas/UserWithRelations" }, + { + type: "object", + properties: { + notifications: { + type: "array", + items: { + $ref: "#/components/schemas/Notification", + }, + }, + }, + }, + ], + }, + }, + }, + }, + apis: ["**/controller/*.ts"], +}; + +const swaggerDocs = swaggerJSdoc(swaggerOptions); + + +console.log(swaggerDocs) +export const swaggerRouter = Router() + + +swaggerRouter.get( + "/json", catchAsync(async (req, res, next) => { + res.json(swaggerDocs); + }) +); +swaggerRouter.use("/", swaggerUi.serve, swaggerUi.setup(swaggerDocs)); \ No newline at end of file diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts new file mode 100644 index 0000000..c172de0 --- /dev/null +++ b/server/src/routes/userRoutes.ts @@ -0,0 +1,13 @@ +import {Router} from "express"; +import {UserController} from "../controller/UserController"; + +export const UserRoutes = Router(); + +const userController = new UserController() + +UserRoutes.route("/").get(userController.getAllUsers) +UserRoutes.route("/me").get(userController.getMe) +UserRoutes.route("/:id").get(userController.getUser) +UserRoutes.route("/follow/:id") + .post(userController.followUser) + .delete(userController.unfollowUser) \ No newline at end of file