Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
19dcf7c660 |
|
|
@ -1,897 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"isPrivate": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "User private status"
|
|
||||||
},
|
|
||||||
"profilePictureId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "User profile picture ID"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patch": {
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": ["Users"],
|
|
||||||
"summary": "Update the currently logged in user",
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"isPrivate": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the user's account is private"
|
|
||||||
},
|
|
||||||
"profilePictureId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The ID of the user's profile picture"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successfully updated the user",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UserWithRelationsAndNotifications"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Invalid request body"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/users/follow/{id}": {
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
65
README.md
65
README.md
|
|
@ -1,65 +0,0 @@
|
||||||
# DevSpace
|
|
||||||
|
|
||||||
DevSpace is a social platform demo made with Express, Typescript, React, Redux, MaterialUI and Node. You can check it out at www.devspace.micosita.es, and there is an api spec at www.api.devspace.micosita/docs
|
|
||||||
|
|
||||||
### Disclaimer
|
|
||||||
This app was made over the course of a week and a half of work. It is by no means production ready, and many things need to be changed for it to be scalable to a sizable userbase. It is fun toy to play with, or to implement new features.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- User authentication
|
|
||||||
- Create, read, update, and delete posts
|
|
||||||
- User search
|
|
||||||
- Follow other users
|
|
||||||
- Notifications
|
|
||||||
- Private or public profiles
|
|
||||||
- Responsive design
|
|
||||||
- Liking and commenting posts
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
What things you need to install the software and how to install them:
|
|
||||||
|
|
||||||
- Node.js
|
|
||||||
- npm
|
|
||||||
- docker
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. Clone the repo
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/micosilent/devspace.git
|
|
||||||
```
|
|
||||||
2. Set the docker-compose.yml to match your environment, pay special attention at the secrets, and routes set in the environment variables.
|
|
||||||
|
|
||||||
3. Run the docker stack
|
|
||||||
```sh
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
We are done!
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
|
|
||||||
|
|
||||||
1. Fork the Project
|
|
||||||
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
### TODO:
|
|
||||||
|
|
||||||
- [ ] Implement image uploads (using multer and multipart forms)
|
|
||||||
- [ ] Real time chat feature (using websockets)
|
|
||||||
- [ ] Implement a proper paging middleware in the backend for typeORM
|
|
||||||
- [ ] Refreshing the page causes requests to be sent without JWT
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Distributed under the MIT License. See `LICENSE` for more information.
|
|
||||||
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
|
||||||
"spaces": 2,
|
|
||||||
"generator-cli": {
|
|
||||||
"version": "7.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,32 +3,18 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"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/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.77",
|
"@types/node": "^16.18.77",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18"
|
"@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"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import logo from './logo.svg';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<img src={logo} className="App-logo" alt="logo" />
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to reload.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className="App-link"
|
||||||
|
href="https://reactjs.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Learn React
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
wwwroot/*.js
|
|
||||||
node_modules
|
|
||||||
typings
|
|
||||||
dist
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
.gitignore
|
|
||||||
.npmignore
|
|
||||||
api.ts
|
|
||||||
base.ts
|
|
||||||
common.ts
|
|
||||||
configuration.ts
|
|
||||||
git_push.sh
|
|
||||||
index.ts
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
7.3.0
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,86 +0,0 @@
|
||||||
/* 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 = {
|
|
||||||
}
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
/* 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 <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
|
||||||
const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};
|
|
||||||
return axios.request<T, R>(axiosRequestArgs);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/* 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<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
|
||||||
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<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
|
||||||
/**
|
|
||||||
* 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<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
|
||||||
/**
|
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
#!/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'
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/* 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";
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { ThemeProvider } from "@emotion/react";
|
|
||||||
import React from "react";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { selectDarkMode } from "./loginSlice";
|
|
||||||
import { createTheme } from "@mui/material";
|
|
||||||
|
|
||||||
const AppThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const darkThemeEnabled = useSelector(selectDarkMode);
|
|
||||||
|
|
||||||
const defaultTheme = createTheme({
|
|
||||||
palette: {
|
|
||||||
mode: darkThemeEnabled ? "dark" : "light",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return <ThemeProvider theme={defaultTheme}>{children}</ThemeProvider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppThemeProvider;
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
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;
|
|
||||||
darkMode: boolean;
|
|
||||||
userInfo: {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
jwt: string;
|
|
||||||
id: number;
|
|
||||||
profilePictureId: string;
|
|
||||||
notifications: Notification[];
|
|
||||||
isPrivate: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const storageDarkMode = localStorage.getItem("darkMode") === "true";
|
|
||||||
|
|
||||||
const initialState: loginState = {
|
|
||||||
loggedIn: false,
|
|
||||||
status: Status.idle,
|
|
||||||
darkMode: storageDarkMode || false,
|
|
||||||
error: null,
|
|
||||||
userInfo: {
|
|
||||||
isPrivate: false,
|
|
||||||
firstName: "",
|
|
||||||
lastName: "",
|
|
||||||
jwt: "",
|
|
||||||
id: -1,
|
|
||||||
profilePictureId: "",
|
|
||||||
notifications: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loginSlice = createSlice({
|
|
||||||
name: "login",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
login: (state, action) => {
|
|
||||||
state.loggedIn = true;
|
|
||||||
state.userInfo.id = action.payload.id;
|
|
||||||
state.userInfo.jwt = action.payload.jwt;
|
|
||||||
state.userInfo.firstName = action.payload.firstName;
|
|
||||||
state.userInfo.lastName = action.payload.lastName;
|
|
||||||
state.userInfo.profilePictureId = action.payload.profilePictureId;
|
|
||||||
state.userInfo.notifications = action.payload.notifications;
|
|
||||||
state.userInfo.isPrivate = action.payload.isPrivate;
|
|
||||||
},
|
|
||||||
logoff: (state) => {
|
|
||||||
state.loggedIn = false;
|
|
||||||
state.userInfo = initialState.userInfo;
|
|
||||||
},
|
|
||||||
setStatus: (state, action) => {
|
|
||||||
state.status = action.payload;
|
|
||||||
},
|
|
||||||
setError: (state, action) => {
|
|
||||||
state.error = action.payload;
|
|
||||||
},
|
|
||||||
setDarkMode: (state, action) => {
|
|
||||||
state.darkMode = action.payload;
|
|
||||||
},
|
|
||||||
setPrivate: (state, action) => {
|
|
||||||
state.userInfo.isPrivate = 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,
|
|
||||||
id: userResponse.data.id,
|
|
||||||
profilePictureId: userResponse.data.profilePictureId,
|
|
||||||
notifications: userResponse.data.notifications,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
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 updateMe =
|
|
||||||
(isPrivate: boolean): AppThunk =>
|
|
||||||
async (dispatch) => {
|
|
||||||
const userApi = new UsersApi(
|
|
||||||
new Configuration({
|
|
||||||
basePath: process.env.REACT_APP_BACKEND_URL,
|
|
||||||
accessToken: localStorage.getItem("jwt") || "",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log("From inside updateME dispatch, param is:", isPrivate);
|
|
||||||
|
|
||||||
await userApi.usersMePatch({ isPrivate });
|
|
||||||
const userResponse = await userApi.usersMeGet();
|
|
||||||
dispatch(setPrivate(userResponse.data.isPrivate));
|
|
||||||
};
|
|
||||||
|
|
||||||
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, setDarkMode, setPrivate } =
|
|
||||||
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;
|
|
||||||
|
|
||||||
export const selectDarkMode = (state: { login: loginState }) =>
|
|
||||||
state.login.darkMode;
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
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[];
|
|
||||||
aPost: Post | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: postSliceInterface = {
|
|
||||||
status: Status.idle,
|
|
||||||
followedPosts: [],
|
|
||||||
globalPosts: [],
|
|
||||||
aPost: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
setAPost: (state, action) => {
|
|
||||||
state.aPost = 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, postType?: "global" | "followed"): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.likePost(postId);
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
|
|
||||||
if (postType === "global") {
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
} else if (postType === "followed") {
|
|
||||||
dispatch(fetchFollowedPosts());
|
|
||||||
} else {
|
|
||||||
dispatch(fetchFollowedPosts());
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unLikePost =
|
|
||||||
(postId: number, postType?: "global" | "followed"): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.unlikePost(postId);
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
|
|
||||||
if (postType === "global") {
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
} else if (postType === "followed") {
|
|
||||||
dispatch(fetchFollowedPosts());
|
|
||||||
} else {
|
|
||||||
dispatch(fetchFollowedPosts());
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAPost =
|
|
||||||
(postId: number): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
const response = await postApi.getPost(postId);
|
|
||||||
dispatch(setAPost(response.data));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commentAPost =
|
|
||||||
(postId: number, comment: string): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.commentPost(postId, { content: comment });
|
|
||||||
dispatch(fetchAPost(postId));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPost =
|
|
||||||
(title: string, content: string): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.createPost({ title, content });
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePost =
|
|
||||||
(postId: number, title: string, content: string): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.updatePost(postId, { title, content });
|
|
||||||
dispatch(fetchAPost(postId));
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deletePost =
|
|
||||||
(postId: number): AppThunk =>
|
|
||||||
async (dispatch: any) => {
|
|
||||||
const postApi = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
await postApi.deletePost(postId);
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const { setFollowedPosts, setGlobalPosts, setStatus, setAPost } =
|
|
||||||
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;
|
|
||||||
export const selectAPost = (state: any) => state.post.aPost;
|
|
||||||
|
|
||||||
function createApi(store: Store) {
|
|
||||||
return new PostsApi(
|
|
||||||
new Configuration({
|
|
||||||
basePath: process.env.REACT_APP_BACKEND_URL,
|
|
||||||
accessToken: store.getState().login.userInfo.jwt,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import loginReducer from "./loginSlice";
|
|
||||||
import postReducer from "./postSlice";
|
|
||||||
import usersReducer from "./usersSlice";
|
|
||||||
|
|
||||||
export const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
login: loginReducer,
|
|
||||||
post: postReducer,
|
|
||||||
users: usersReducer,
|
|
||||||
},
|
|
||||||
devTools: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AppDispatch = typeof store.dispatch;
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
|
||||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
|
||||||
ReturnType,
|
|
||||||
RootState,
|
|
||||||
unknown,
|
|
||||||
Action<string>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import { Store, createSlice } from "@reduxjs/toolkit";
|
|
||||||
import { Configuration, User, UserWithRelations, UsersApi } from "../api";
|
|
||||||
import { Status } from "../util/types";
|
|
||||||
import { AppThunk, store } from "./store";
|
|
||||||
|
|
||||||
interface userState {
|
|
||||||
status: Status;
|
|
||||||
error: string | null;
|
|
||||||
users: User[];
|
|
||||||
userWithRelations: UserWithRelations | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: userState = {
|
|
||||||
status: Status.idle,
|
|
||||||
error: null,
|
|
||||||
users: [],
|
|
||||||
userWithRelations: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usersSlice = createSlice({
|
|
||||||
name: "users",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setUsers: (state, action) => {
|
|
||||||
state.users = action.payload;
|
|
||||||
},
|
|
||||||
setStatus: (state, action) => {
|
|
||||||
state.status = action.payload;
|
|
||||||
},
|
|
||||||
setError: (state, action) => {
|
|
||||||
state.error = action.payload;
|
|
||||||
},
|
|
||||||
setUserWithRelations: (state, action) => {
|
|
||||||
state.userWithRelations = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getUsers = (): AppThunk => async (dispatch) => {
|
|
||||||
const api = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
try {
|
|
||||||
const response = await api.usersGet();
|
|
||||||
dispatch(setUsers(response.data));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(setError((error as Error).message));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserWithRelations =
|
|
||||||
(userId: number): AppThunk =>
|
|
||||||
async (dispatch) => {
|
|
||||||
const api = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
try {
|
|
||||||
const response = await api.usersIdGet(userId);
|
|
||||||
dispatch(setUserWithRelations(response.data));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(setError((error as Error).message));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const followUser =
|
|
||||||
(userId: number): AppThunk =>
|
|
||||||
async (dispatch) => {
|
|
||||||
const api = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
try {
|
|
||||||
console.log("Trying to follow: ", userId);
|
|
||||||
await api.usersFollowIdPost(userId);
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(setError((error as Error).message));
|
|
||||||
} finally {
|
|
||||||
dispatch(getUserWithRelations(userId));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unFollowUser =
|
|
||||||
(userId: number): AppThunk =>
|
|
||||||
async (dispatch) => {
|
|
||||||
const api = createApi(store);
|
|
||||||
|
|
||||||
dispatch(setStatus(Status.loading));
|
|
||||||
try {
|
|
||||||
console.log("Trying to unfollow: ", userId);
|
|
||||||
await api.usersFollowIdDelete(userId);
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(setError((error as Error).message));
|
|
||||||
} finally {
|
|
||||||
dispatch(getUserWithRelations(userId));
|
|
||||||
dispatch(setStatus(Status.idle));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const { setUsers, setStatus, setError, setUserWithRelations } =
|
|
||||||
usersSlice.actions;
|
|
||||||
|
|
||||||
export default usersSlice.reducer;
|
|
||||||
|
|
||||||
export const selectUsers = (state: { users: userState }) => state.users.users;
|
|
||||||
export const selectAUser = (state: { users: userState }) =>
|
|
||||||
state.users.userWithRelations;
|
|
||||||
export const selectStatus = (state: { users: userState }) => state.users.status;
|
|
||||||
export const selectError = (state: { users: userState }) => state.users.error;
|
|
||||||
|
|
||||||
function createApi(store: Store) {
|
|
||||||
const configuration = new Configuration({
|
|
||||||
basePath: process.env.REACT_APP_BACKEND_URL,
|
|
||||||
accessToken: store.getState().login.userInfo.jwt,
|
|
||||||
});
|
|
||||||
return new UsersApi(configuration);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Link, Typography } from "@mui/material";
|
|
||||||
|
|
||||||
export function Copyright(props: any) {
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
align="center"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{"Copyright © "}
|
|
||||||
<Link color="inherit" href="https://tutrastero.com/">
|
|
||||||
Tu Trastero Tu Otro Espacio S.L
|
|
||||||
</Link>{" "}
|
|
||||||
{new Date().getFullYear()}.
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import {Grid, Hidden, Typography, Drawer, Box} from "@mui/material";
|
|
||||||
|
|
||||||
export default function ResponsiveDrawer(){
|
|
||||||
const drawer = (
|
|
||||||
<div>
|
|
||||||
<Grid container direction="column">
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h6">Home</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h6">Search</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h6">Notifications</Typography>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
|
||||||
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
|
|
||||||
<Hidden smUp implementation="css">
|
|
||||||
<Drawer
|
|
||||||
variant="temporary"
|
|
||||||
anchor={'bottom'}
|
|
||||||
open={true}
|
|
||||||
ModalProps={{
|
|
||||||
keepMounted: true, // Better open performance on mobile.
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drawer}
|
|
||||||
</Drawer>
|
|
||||||
</Hidden>
|
|
||||||
<Hidden xsDown implementation="css">
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
open
|
|
||||||
>
|
|
||||||
{drawer}
|
|
||||||
</Drawer>
|
|
||||||
</Hidden>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { Divider, styled } from "@mui/material";
|
|
||||||
|
|
||||||
export const StyledDivider = styled(Divider)({
|
|
||||||
marginBottom: "2%",
|
|
||||||
});
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { Avatar } from "@mui/material";
|
|
||||||
import { User, UserWithRelations } from "../api";
|
|
||||||
|
|
||||||
interface AppAvatarProps {
|
|
||||||
user: User | UserWithRelations;
|
|
||||||
small?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppAvatar(props: AppAvatarProps) {
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
alt={`${props.user.firstName} ${props.user.lastName}`}
|
|
||||||
src={`/images/${props.user.profilePictureId}`}
|
|
||||||
sx={{
|
|
||||||
width: props.small ? "24px" : "42px",
|
|
||||||
height: props.small ? "24px" : "42px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { BottomNavigation, BottomNavigationAction, Paper } from "@mui/material";
|
|
||||||
import { useEffect, 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 { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function BottomAppBar() {
|
|
||||||
const [value, setValue] = useState("");
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(location.pathname.replace("/", ""));
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
const handleClick = (to: string) => () => {
|
|
||||||
navigate(to);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}
|
|
||||||
elevation={8}
|
|
||||||
>
|
|
||||||
<BottomNavigation
|
|
||||||
showLabels
|
|
||||||
value={value}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setValue(newValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="My Feed"
|
|
||||||
value="feed"
|
|
||||||
icon={<FeedIcon />}
|
|
||||||
onClick={handleClick("feed")}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="Global Feed"
|
|
||||||
value="global"
|
|
||||||
icon={<GlobalIcon />}
|
|
||||||
onClick={handleClick("global")}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="New Post"
|
|
||||||
value="newpost"
|
|
||||||
icon={<AddIcon />}
|
|
||||||
onClick={handleClick("newpost")}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="Search"
|
|
||||||
value="search"
|
|
||||||
icon={<SearchIcon />}
|
|
||||||
onClick={handleClick("search")}
|
|
||||||
/>
|
|
||||||
</BottomNavigation>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
|
||||||
import { Badge, Tooltip } from "@mui/material";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function NotificationBell() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const handleClick = () => {
|
|
||||||
navigate("/notifications");
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Badge badgeContent={0} color="secondary">
|
|
||||||
<Tooltip title="Notifications">
|
|
||||||
<NotificationsIcon onClick={handleClick} />
|
|
||||||
</Tooltip>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
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: "feed" | "user";
|
|
||||||
feedType?: "global" | "followed";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PostListItem(props: PostListItemProps) {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const userInfo = useSelector(selectUserInfo);
|
|
||||||
const [liked, setLiked] = useState(
|
|
||||||
props.post.likedBy?.some((user) => user.id === userInfo.id) || false
|
|
||||||
);
|
|
||||||
const [numberOfLikes, setNumberOfLikes] = useState(
|
|
||||||
props.post.likedBy?.length || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLiked(
|
|
||||||
props.post.likedBy?.some((user) => user.id === userInfo.id) || false
|
|
||||||
);
|
|
||||||
setNumberOfLikes(props.post.likedBy?.length || 0);
|
|
||||||
}, [props.post, userInfo.id]);
|
|
||||||
|
|
||||||
const handleLike = () => {
|
|
||||||
if (!liked) {
|
|
||||||
setNumberOfLikes(numberOfLikes + 1);
|
|
||||||
dispatch(likePost(props.post.id as number, props.feedType));
|
|
||||||
}
|
|
||||||
// If the user has already liked the post, unlike it
|
|
||||||
else {
|
|
||||||
setNumberOfLikes(numberOfLikes - 1);
|
|
||||||
dispatch(unLikePost(props.post.id as number, props.feedType));
|
|
||||||
}
|
|
||||||
setLiked(!liked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePostClick = () => {
|
|
||||||
navigate(`/post/${props.post.id}`);
|
|
||||||
};
|
|
||||||
const handleUserClick = () => {
|
|
||||||
navigate(`/user/${props.post.createdBy?.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
mt: "1rem",
|
|
||||||
justifyContent: "start",
|
|
||||||
alignItems: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleUserClick}
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
display: `${props.postType === "user" ? "none" : "block"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
gutterBottom
|
|
||||||
sx={{ textAlign: "left", width: "100%" }}
|
|
||||||
variant="body2"
|
|
||||||
component="div"
|
|
||||||
color="text.secondary"
|
|
||||||
>
|
|
||||||
{props.post.createdBy?.firstName} {props.post.createdBy?.lastName}
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
<Card sx={{ maxWidth: 400, width: "100%" }}>
|
|
||||||
<CardActionArea sx={{ mb: "0.8rem" }} onClick={handlePostClick}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography gutterBottom variant="h5" component="div">
|
|
||||||
{props.post.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6" color="text.secondary">
|
|
||||||
{props.post.content}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</CardActionArea>
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
spacing={1}
|
|
||||||
sx={{ display: "flex", justifyContent: "end" }}
|
|
||||||
>
|
|
||||||
<StyledGrid item>
|
|
||||||
<Button onClick={handleLike}>
|
|
||||||
<StyledTypography variant="body2" color="text.secondary">
|
|
||||||
{numberOfLikes}
|
|
||||||
</StyledTypography>
|
|
||||||
|
|
||||||
{liked ? <ThumbUpAltIcon /> : <ThumbUpOffAltIcon />}
|
|
||||||
</Button>
|
|
||||||
</StyledGrid>
|
|
||||||
<StyledGrid item>
|
|
||||||
<Button onClick={handlePostClick}>
|
|
||||||
<StyledTypography variant="body2" color="text.secondary">
|
|
||||||
{props.post.comments?.length || 0}
|
|
||||||
</StyledTypography>
|
|
||||||
<MessageIcon />
|
|
||||||
</Button>
|
|
||||||
</StyledGrid>
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
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 (
|
|
||||||
<Drawer
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
"& .MuiDrawer-paper": {
|
|
||||||
width: drawerWidth,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
variant="permanent"
|
|
||||||
anchor="left"
|
|
||||||
>
|
|
||||||
<Toolbar />
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<List>
|
|
||||||
<ListItem key={"myfeed"} disablePadding onClick={handleClick("feed")}>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
<FeedIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={"My feed"} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
key={"globalfeed"}
|
|
||||||
disablePadding
|
|
||||||
onClick={handleClick("global")}
|
|
||||||
>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
<GlobalIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={"Global feed"} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
key={"newpost"}
|
|
||||||
disablePadding
|
|
||||||
onClick={handleClick("newpost")}
|
|
||||||
>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
<AddIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={"New Post"} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListItem key={"search"} disablePadding onClick={handleClick("search")}>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
<SearchIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={"Search"} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
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 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";
|
|
||||||
import { AppAvatar } from "./appAvatar";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
interface TopAppBarProps {
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TopAppBar(props: TopAppBarProps) {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const userInfo = useSelector(selectUserInfo);
|
|
||||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorElUser(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseUserMenu = () => {
|
|
||||||
setAnchorElUser(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
dispatch(postLogout());
|
|
||||||
setAnchorElUser(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProfileClick = () => {
|
|
||||||
navigate("/me");
|
|
||||||
setAnchorElUser(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSettingsClick = () => {
|
|
||||||
navigate("/settings");
|
|
||||||
setAnchorElUser(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBar
|
|
||||||
position="fixed"
|
|
||||||
sx={{ zIndex: 1600, height: `${props.height}px` }}
|
|
||||||
>
|
|
||||||
<Toolbar disableGutters>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
noWrap
|
|
||||||
component="a"
|
|
||||||
sx={{
|
|
||||||
marginLeft: "1rem",
|
|
||||||
mr: 2,
|
|
||||||
display: "flex",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: ".3rem",
|
|
||||||
color: "inherit",
|
|
||||||
textDecoration: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
DevSpace
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "right",
|
|
||||||
padding: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NotificationBell />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 0, mr: "2rem" }}>
|
|
||||||
<Tooltip title="Open settings">
|
|
||||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
|
||||||
<AppAvatar user={userInfo} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
sx={{ mt: "45px" }}
|
|
||||||
id="menu-appbar"
|
|
||||||
anchorEl={anchorElUser}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
open={Boolean(anchorElUser)}
|
|
||||||
onClose={handleCloseUserMenu}
|
|
||||||
>
|
|
||||||
<MenuItem key={"profile"} onClick={handleProfileClick}>
|
|
||||||
<Typography textAlign="center">{"Profile"}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key={"settings"} onClick={handleSettingsClick}>
|
|
||||||
<Typography textAlign="center">{"Settings"}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key={"logout"} onClick={handleLogout}>
|
|
||||||
<Typography textAlign="center">{"Logout"}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default TopAppBar;
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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 (
|
|
||||||
<div id="error-page">
|
|
||||||
<Box sx={{
|
|
||||||
marginTop: 16,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center"
|
|
||||||
}}>
|
|
||||||
<Typography component={"h1"} variant={"h1"}>
|
|
||||||
Whoops!
|
|
||||||
</Typography>
|
|
||||||
<Typography component={"p"}>
|
|
||||||
Something went wrong :(
|
|
||||||
</Typography>
|
|
||||||
<span style={{height: 16}}/>
|
|
||||||
<Typography component={"p"}>
|
|
||||||
<i>{error.message || error.statusText}</i>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import "./index.css";
|
import './index.css';
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import App from './App';
|
||||||
import "@fontsource/roboto";
|
import reportWebVitals from './reportWebVitals';
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|
||||||
import Root from "./routes/root";
|
|
||||||
import ErrorPage from "./error-page";
|
|
||||||
import { CssBaseline } 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 PostView from "./routes/post";
|
|
||||||
import NewPost from "./routes/newPost";
|
|
||||||
import Search from "./routes/search";
|
|
||||||
import Settings from "./routes/settings";
|
|
||||||
import ANotification from "./routes/notification";
|
|
||||||
import AppThemeProvider from "./app/AppThemeProvider";
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById("root") as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <Root />,
|
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "feed",
|
|
||||||
element: <PostList type="feed" feedType="followed" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "global",
|
|
||||||
element: <PostList type="feed" feedType="global" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "me",
|
|
||||||
element: <Profile selfProfile />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "user/:id",
|
|
||||||
element: <Profile />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "post/:id",
|
|
||||||
element: <PostView />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "post/:id/edit",
|
|
||||||
element: <NewPost />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "search",
|
|
||||||
element: <Search />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "newpost",
|
|
||||||
element: <NewPost />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "settings",
|
|
||||||
element: <Settings />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "notifications",
|
|
||||||
element: <ANotification />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/auth",
|
|
||||||
element: <AuthRoot />,
|
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "login",
|
|
||||||
element: <Login />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "register",
|
|
||||||
element: <Register />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<App />
|
||||||
<AppThemeProvider>
|
|
||||||
<CssBaseline />
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</AppThemeProvider>
|
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
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 (
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
sx={{
|
|
||||||
minHeight: "100vh",
|
|
||||||
backgroundColor: (theme) =>
|
|
||||||
theme.palette.mode === "light"
|
|
||||||
? theme.palette.grey[100]
|
|
||||||
: theme.palette.grey[900],
|
|
||||||
}}
|
|
||||||
direction="column"
|
|
||||||
spacing={5}
|
|
||||||
>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
container
|
|
||||||
alignItems="center"
|
|
||||||
direction="column"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<Typography variant="h1" fontSize={"4.5rem"}>
|
|
||||||
DevSpace
|
|
||||||
<StyledDivider />
|
|
||||||
</Typography>
|
|
||||||
<Grid item>
|
|
||||||
<Outlet />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Copyright />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
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<HTMLFormElement>) => {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Container maxWidth="sm" sx={{ marginTop: "15%" }}>
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="column"
|
|
||||||
justifyContent={"center"}
|
|
||||||
alignItems={"center"}
|
|
||||||
spacing={2}
|
|
||||||
sx={{ padding: "10%" }}
|
|
||||||
>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
component="form"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
container
|
|
||||||
direction="column"
|
|
||||||
spacing={3}
|
|
||||||
>
|
|
||||||
<Grid item>
|
|
||||||
<StyledTypography>Email:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label="Email"
|
|
||||||
name="email"
|
|
||||||
variant="outlined"
|
|
||||||
autoComplete="email"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<StyledTypography>Password:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label="Password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
variant="outlined"
|
|
||||||
error={!!errorMessage}
|
|
||||||
helperText={errorMessage}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<FormControlLabel
|
|
||||||
id="longExpiration"
|
|
||||||
control={<Checkbox value="remember" color="primary" />}
|
|
||||||
label="Remember me"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button type="submit" variant="contained" fullWidth={true}>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<StyledDivider />
|
|
||||||
<Typography variant="h5" fontSize={"1.5rem"}>
|
|
||||||
Don't have an account yet?
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<StyledGrid item>
|
|
||||||
<Link href="../auth/register" variant="body2">
|
|
||||||
Register now!
|
|
||||||
</Link>
|
|
||||||
</StyledGrid>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledGrid = styled(Grid)({
|
|
||||||
width: "50%",
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledTypography = styled(Typography)({
|
|
||||||
marginBottom: "5%",
|
|
||||||
});
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
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<HTMLFormElement>) => {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Container maxWidth="sm" sx={{ marginTop: "5%" }}>
|
|
||||||
<Grid
|
|
||||||
component="form"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
container
|
|
||||||
direction="column"
|
|
||||||
justifyContent={"center"}
|
|
||||||
alignItems={"center"}
|
|
||||||
spacing={2}
|
|
||||||
sx={{ paddingBottom: "10%" }}
|
|
||||||
>
|
|
||||||
<Grid item container xs={12} spacing={2} sx={{ width: "70%" }}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<StyledTypography>First Name:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="First Name"
|
|
||||||
name="firstName"
|
|
||||||
required
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<StyledTypography>Last Name:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
name="lastName"
|
|
||||||
required
|
|
||||||
label="Last Name"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<StyledTypography>Email:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<StyledTypography>Password:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
label="Password"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<StyledTypography>Password Confirmation:</StyledTypography>
|
|
||||||
<TextField
|
|
||||||
type="password"
|
|
||||||
name="passwordConfirmation"
|
|
||||||
label="Password Confirmation"
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<StyledGrid item xs={12}>
|
|
||||||
<Button type="submit" variant="contained" fullWidth={true}>
|
|
||||||
Register
|
|
||||||
</Button>
|
|
||||||
</StyledGrid>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h5" fontSize={"1.5rem"}>
|
|
||||||
Already have an account?
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<StyledGrid item>
|
|
||||||
<Link href="../auth/login" variant="body2">
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
</StyledGrid>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledGrid = styled(Grid)({
|
|
||||||
width: "50%",
|
|
||||||
});
|
|
||||||
const StyledTypography = styled(Typography)({
|
|
||||||
marginBottom: "5%",
|
|
||||||
});
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import { Box, Button, TextField, Typography } from "@mui/material";
|
|
||||||
import { useAppDispatch } from "../app/store";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import {
|
|
||||||
createPost,
|
|
||||||
selectAPost,
|
|
||||||
updatePost,
|
|
||||||
} from "../app/postSlice";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
|
|
||||||
export default function NewPost() {
|
|
||||||
const { id } = useParams();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [newPostText, setNewPostText] = React.useState("");
|
|
||||||
const [newPostTitle, setNewPostTitle] = React.useState("");
|
|
||||||
const aPost = useSelector(selectAPost);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (id) {
|
|
||||||
setNewPostText(aPost.content);
|
|
||||||
setNewPostTitle(aPost.title);
|
|
||||||
}
|
|
||||||
}, [aPost, id]);
|
|
||||||
|
|
||||||
const handleCreatePost = () => {
|
|
||||||
if (newPostText === "" || newPostTitle === "") return;
|
|
||||||
|
|
||||||
dispatch(createPost(newPostTitle, newPostText));
|
|
||||||
setNewPostText("");
|
|
||||||
setNewPostTitle("");
|
|
||||||
navigate("/global");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePost = () => {
|
|
||||||
if (newPostText === "" || newPostTitle === "") return;
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
dispatch(updatePost(parseInt(id), newPostTitle, newPostText));
|
|
||||||
setNewPostText("");
|
|
||||||
setNewPostTitle("");
|
|
||||||
navigate("/global");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
margin: "auto",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4" sx={{ marginBottom: 2 }}>
|
|
||||||
Share your thoughts!
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
label="Title"
|
|
||||||
value={newPostTitle}
|
|
||||||
multiline
|
|
||||||
onChange={(e) => setNewPostTitle(e.target.value)}
|
|
||||||
sx={{ marginBottom: 2 }}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Post"
|
|
||||||
value={newPostText}
|
|
||||||
onChange={(e) => setNewPostText(e.target.value)}
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
sx={{ marginBottom: 2 }}
|
|
||||||
/>
|
|
||||||
{id ? (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="success"
|
|
||||||
fullWidth
|
|
||||||
onClick={handleUpdatePost}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
fullWidth
|
|
||||||
onClick={handleCreatePost}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { selectUserInfo } from "../app/loginSlice";
|
|
||||||
import { Box, List, ListItem, Paper } from "@mui/material";
|
|
||||||
|
|
||||||
export default function ANotification() {
|
|
||||||
const userInfo = useSelector(selectUserInfo);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<List sx={{ width: "100%" }}>
|
|
||||||
{userInfo.notifications.length === 0 ? (
|
|
||||||
<ListItem sx={{ width: "100%" }}>No notifications</ListItem>
|
|
||||||
) : (
|
|
||||||
userInfo.notifications.map((notification: any) => (
|
|
||||||
<ListItem key={notification.id} sx={{ width: "100%" }}>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
margin: "1rem",
|
|
||||||
padding: "0.2rem 1rem 0.2rem 1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{notification.message}
|
|
||||||
</Paper>
|
|
||||||
</ListItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
commentAPost,
|
|
||||||
deletePost,
|
|
||||||
fetchAPost,
|
|
||||||
selectAPost,
|
|
||||||
} from "../app/postSlice";
|
|
||||||
import { useAppDispatch } from "../app/store";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import React from "react";
|
|
||||||
import { Post } from "../api";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { AppAvatar } from "../components/appAvatar";
|
|
||||||
import { selectUserInfo } from "../app/loginSlice";
|
|
||||||
|
|
||||||
export default function PostView() {
|
|
||||||
const postId = useParams<{ id: string }>().id;
|
|
||||||
const storePost = useSelector(selectAPost);
|
|
||||||
const selfUser = useSelector(selectUserInfo);
|
|
||||||
const [postToDisplay, setPostToDisplay] = React.useState<Post | null>(null);
|
|
||||||
const [newComentText, setNewCommentText] = React.useState("");
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchAPost(parseInt(postId!, 10)));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPostToDisplay(storePost);
|
|
||||||
}, [storePost]);
|
|
||||||
|
|
||||||
const handleCommentPost = () => {
|
|
||||||
if (newComentText.length > 0) {
|
|
||||||
dispatch(commentAPost(postToDisplay!.id!, newComentText));
|
|
||||||
setNewCommentText("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelComment = () => {
|
|
||||||
setNewCommentText("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePost = () => {
|
|
||||||
dispatch(deletePost(postToDisplay!.id!));
|
|
||||||
navigate("/global");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "60%",
|
|
||||||
margin: "auto",
|
|
||||||
minWidth: "330px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
<Paper>
|
|
||||||
<Box sx={{ margin: "1rem" }}>
|
|
||||||
<Box
|
|
||||||
sx={{ display: "flex", alignItems: "center", paddingTop: "1rem" }}
|
|
||||||
>
|
|
||||||
{postToDisplay?.createdBy && (
|
|
||||||
<AppAvatar user={postToDisplay?.createdBy!} />
|
|
||||||
)}
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
sx={{ mb: "0.2rem", ml: "0.3rem" }}
|
|
||||||
>
|
|
||||||
{postToDisplay?.createdBy?.firstName}{" "}
|
|
||||||
{postToDisplay?.createdBy?.lastName}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Divider sx={{ mb: "0.3rem" }} />
|
|
||||||
<Typography variant="h4">{postToDisplay?.title}</Typography>
|
|
||||||
<Typography variant="body1">{postToDisplay?.content}</Typography>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
{postToDisplay?.createdBy?.id === selfUser?.id && (
|
|
||||||
<Box>
|
|
||||||
<Button onClick={() => navigate("edit")}>Edit</Button>
|
|
||||||
<Button color="warning" onClick={handleDeletePost}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<TextField
|
|
||||||
label="Add a comment..."
|
|
||||||
fullWidth
|
|
||||||
multiline
|
|
||||||
value={newComentText}
|
|
||||||
onChange={(e) => setNewCommentText(e.target.value)}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleCommentPost}
|
|
||||||
sx={{
|
|
||||||
display: `${newComentText.length > 0 ? "inherit" : "none"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleCancelComment}
|
|
||||||
sx={{
|
|
||||||
display: `${newComentText.length > 0 ? "inherit" : "none"}`,
|
|
||||||
ml: "0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
mb: "1rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
{postToDisplay?.comments?.map((comment) => (
|
|
||||||
<Paper>
|
|
||||||
<Box sx={{ margin: "1rem" }}>
|
|
||||||
<Box sx={{ display: "flex", mb: "0.3rem", pt: "0.3rem" }}>
|
|
||||||
<AppAvatar user={comment.createdBy!} small />
|
|
||||||
<Typography variant="subtitle2" sx={{ ml: "0.5rem" }}>
|
|
||||||
{comment.createdBy?.firstName} {comment.createdBy?.lastName}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Divider sx={{ mb: "0.3rem" }} />
|
|
||||||
<Typography variant="body1">{comment.content}</Typography>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import {
|
|
||||||
fetchFollowedPosts,
|
|
||||||
fetchGlobalPosts,
|
|
||||||
selectAllPosts,
|
|
||||||
selectFollowedPosts,
|
|
||||||
selectStatus,
|
|
||||||
} from "../app/postSlice";
|
|
||||||
import { store, useAppDispatch } from "../app/store";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Box, CircularProgress, List, Typography } from "@mui/material";
|
|
||||||
import { Post } from "../api";
|
|
||||||
import PostListItem from "../components/postListItem";
|
|
||||||
import { Status } from "../util/types";
|
|
||||||
|
|
||||||
interface PostListProps {
|
|
||||||
type: "feed" | "user";
|
|
||||||
feedType?: "global" | "followed";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PostList(props: PostListProps) {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const followedPosts = useSelector(selectFollowedPosts);
|
|
||||||
const globalPosts = useSelector(selectAllPosts);
|
|
||||||
const status = useSelector(selectStatus);
|
|
||||||
const [posts, setPosts] = useState<Post[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.feedType === "global") {
|
|
||||||
dispatch(fetchGlobalPosts());
|
|
||||||
} else {
|
|
||||||
dispatch(fetchFollowedPosts());
|
|
||||||
}
|
|
||||||
}, [props.feedType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let postsToDisplay: Post[];
|
|
||||||
if (props.feedType === "global") {
|
|
||||||
postsToDisplay = globalPosts;
|
|
||||||
} else {
|
|
||||||
postsToDisplay = followedPosts;
|
|
||||||
}
|
|
||||||
let sortedPosts: Post[] = [];
|
|
||||||
|
|
||||||
if (postsToDisplay.length > 0) {
|
|
||||||
sortedPosts = [...postsToDisplay].sort((a: Post, b: Post) => {
|
|
||||||
return b.createdAt! > a.createdAt!
|
|
||||||
? 1
|
|
||||||
: b.createdAt! < a.createdAt!
|
|
||||||
? -1
|
|
||||||
: 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setPosts(sortedPosts);
|
|
||||||
}, [followedPosts, globalPosts, props.feedType]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
alignContent: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{status === Status.loading ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{props.feedType === "global" && (
|
|
||||||
<List>
|
|
||||||
{posts.map((post: Post) => (
|
|
||||||
<PostListItem
|
|
||||||
post={post}
|
|
||||||
postType="feed"
|
|
||||||
feedType="global"
|
|
||||||
key={post.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
{props.feedType === "followed" && (
|
|
||||||
<List>
|
|
||||||
{followedPosts.map((post: Post) => (
|
|
||||||
<PostListItem
|
|
||||||
post={post}
|
|
||||||
postType="feed"
|
|
||||||
feedType="followed"
|
|
||||||
key={post.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{followedPosts.length === 0 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h3"
|
|
||||||
sx={{ color: "text.secondary", mb: "2rem" }}
|
|
||||||
>
|
|
||||||
Whops!
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
textAlign="center"
|
|
||||||
sx={{ color: "text.secondary" }}
|
|
||||||
>
|
|
||||||
There is no content here! Try following some users, or check
|
|
||||||
the global feed
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
List,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { AppAvatar } from "../components/appAvatar";
|
|
||||||
import { useAppDispatch } from "../app/store";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import {
|
|
||||||
followUser,
|
|
||||||
getUserWithRelations,
|
|
||||||
selectAUser,
|
|
||||||
selectStatus,
|
|
||||||
unFollowUser,
|
|
||||||
} from "../app/usersSlice";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { selectUserInfo } from "../app/loginSlice";
|
|
||||||
import PostListItem from "../components/postListItem";
|
|
||||||
import { Post, UserWithRelations } from "../api";
|
|
||||||
import React from "react";
|
|
||||||
import { Status } from "../util/types";
|
|
||||||
|
|
||||||
export interface ProfileProps {
|
|
||||||
selfProfile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Profile(props: ProfileProps) {
|
|
||||||
const userId = useParams<{ id: string }>().id;
|
|
||||||
const [userToDisplay, setUserToDisplay] =
|
|
||||||
React.useState<UserWithRelations | null>();
|
|
||||||
const aUser = useSelector(selectAUser);
|
|
||||||
const [isFollowed, setIsFollowed] = React.useState(false);
|
|
||||||
const status = useSelector(selectStatus);
|
|
||||||
const selfUser = useSelector(selectUserInfo);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.selfProfile && userId) {
|
|
||||||
dispatch(getUserWithRelations(parseInt(userId, 10)));
|
|
||||||
} else {
|
|
||||||
dispatch(getUserWithRelations(selfUser.id));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUserToDisplay(aUser);
|
|
||||||
if (!props.selfProfile) {
|
|
||||||
setIsFollowed(
|
|
||||||
aUser?.followers?.some((user) => user.id === selfUser.id) || false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [aUser, props.selfProfile, selfUser.id]);
|
|
||||||
|
|
||||||
const handleFollow = () => {
|
|
||||||
if (isFollowed) {
|
|
||||||
dispatch(unFollowUser(userToDisplay!.id!));
|
|
||||||
} else {
|
|
||||||
dispatch(followUser(userToDisplay!.id!));
|
|
||||||
}
|
|
||||||
setIsFollowed((prevState) => !prevState);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{status === Status.loading && (
|
|
||||||
<>
|
|
||||||
<Typography variant="h3">Loading...</Typography>
|
|
||||||
<CircularProgress />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{userToDisplay && status === Status.idle && (
|
|
||||||
<>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
padding: "1rem 1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppAvatar user={userToDisplay!} />
|
|
||||||
<Typography variant="h4" sx={{ ml: "0.8rem" }}>
|
|
||||||
{userToDisplay?.firstName} {userToDisplay?.lastName}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", padding: "1rem 1rem" }}>
|
|
||||||
<Box sx={{ display: "flex" }}>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
sx={{ ml: "0.8rem", mr: "0.5rem" }}
|
|
||||||
>
|
|
||||||
Following:
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6">
|
|
||||||
{userToDisplay?.followed?.length || 0}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex" }}>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
sx={{ ml: "0.8rem", mr: "0.5rem" }}
|
|
||||||
>
|
|
||||||
Followers:{" "}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6">
|
|
||||||
{userToDisplay?.followers?.length || 0}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: `${props.selfProfile ? "none" : "initial"}`,
|
|
||||||
ml: "0.8rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={isFollowed ? "outlined" : "contained"}
|
|
||||||
onClick={handleFollow}
|
|
||||||
>
|
|
||||||
{isFollowed ? "Unfollow" : "Follow"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "75%",
|
|
||||||
margin: "auto",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List>
|
|
||||||
{userToDisplay!.posts?.map((post: Post) => (
|
|
||||||
<PostListItem post={post} postType="user" key={post.id} />
|
|
||||||
)) || (
|
|
||||||
<Typography variant="h3">
|
|
||||||
This user has no posts yet!
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Box>
|
|
||||||
<TopAppBar height={topAppBarHeight} />
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
flexDirection: { xs: "column", sm: "row" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{width > 600 && (
|
|
||||||
<Box sx={{ flexBasis: { sm: "auto" } }}>
|
|
||||||
<SideAppBar drawerWidth={drawerWidth} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
component="main"
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
p: 3,
|
|
||||||
width: { sm: `calc(100% - ${width > 600 ? drawerWidth : 0}px)` },
|
|
||||||
marginTop: { xs: `${topAppBarHeight}px`},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{width <= 600 && (
|
|
||||||
<Box sx={{ flexBasis: "100%" }}>
|
|
||||||
<BottomAppBar />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { getUsers, selectUsers } from "../app/usersSlice";
|
|
||||||
import React, { MouseEventHandler, useEffect } from "react";
|
|
||||||
import { useAppDispatch } from "../app/store";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { AppAvatar } from "../components/appAvatar";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { matchSorter } from "match-sorter";
|
|
||||||
|
|
||||||
export default function Search() {
|
|
||||||
const userList = useSelector(selectUsers);
|
|
||||||
const [displayUserList, setDisplayUserList] = React.useState(userList);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Dispatch the getUsers actions, and set it as the displayUserList
|
|
||||||
// once on the first render
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(getUsers());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDisplayUserList(userList);
|
|
||||||
}, [userList]);
|
|
||||||
|
|
||||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const searchValue = event.currentTarget.value;
|
|
||||||
if (searchValue === "") {
|
|
||||||
setDisplayUserList(userList);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDisplayUserList(
|
|
||||||
matchSorter(userList, searchValue, { keys: ["firstName", "lastName"] })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick: MouseEventHandler = (event) => {
|
|
||||||
const userId = event.currentTarget.id;
|
|
||||||
console.log("User Id from the search component: ", userId);
|
|
||||||
navigate(`/user/${userId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<Paper sx={{ mb: "2rem" }}>
|
|
||||||
<TextField
|
|
||||||
sx={{ width: "100%" }}
|
|
||||||
label="Search for a user..."
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
<Paper>
|
|
||||||
<List>
|
|
||||||
{displayUserList.map((user) => (
|
|
||||||
<Box key={user.id}>
|
|
||||||
<ListItem key={user.id}>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
sx={{ textAlign: "start" }}
|
|
||||||
id={user.id?.toString()}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<AppAvatar user={user} />
|
|
||||||
<ListItemText
|
|
||||||
sx={{ ml: "2rem" }}
|
|
||||||
primary={`${user.firstName} ${user.lastName}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ListItem>
|
|
||||||
<Divider sx={{ mb: "0.3rem" }} />
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { Box, Checkbox, Divider, Typography } from "@mui/material";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import {
|
|
||||||
selectDarkMode,
|
|
||||||
selectUserInfo,
|
|
||||||
setDarkMode,
|
|
||||||
updateMe,
|
|
||||||
} from "../app/loginSlice";
|
|
||||||
import { useAppDispatch } from "../app/store";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function Settings() {
|
|
||||||
const userInfo = useSelector(selectUserInfo);
|
|
||||||
const darkModeState = useSelector(selectDarkMode);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [isPrivate, setIsPrivate] = useState(false);
|
|
||||||
const [darkModeLocal, setDarkModeLocal] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsPrivate(userInfo.isPrivate);
|
|
||||||
}, [userInfo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDarkModeLocal(darkModeState);
|
|
||||||
}, [darkModeState]);
|
|
||||||
|
|
||||||
const handleDarkChange = () => {
|
|
||||||
dispatch(setDarkMode(!darkModeLocal));
|
|
||||||
localStorage.setItem("darkMode", darkModeLocal ? "false" : "true");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrivateChange = () => {
|
|
||||||
dispatch(updateMe(!isPrivate));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box>
|
|
||||||
<Checkbox checked={isPrivate} onChange={handlePrivateChange} /> Private
|
|
||||||
profile
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Box>
|
|
||||||
<Checkbox checked={darkModeLocal} onChange={handleDarkChange} /> Dark
|
|
||||||
mode
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4">More Settings</Typography>
|
|
||||||
<Typography variant="body1">Coming soon...</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export enum Status {
|
|
||||||
idle = "idle",
|
|
||||||
loading = "loading",
|
|
||||||
succeeded = "succeeded",
|
|
||||||
failed = "failed",
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
@ -16,5 +20,7 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ services:
|
||||||
build: ./client
|
build: ./client
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
REACT_APP_BACKEND_URL: http://localhost:3000
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:3000"
|
- "8080:3000"
|
||||||
server:
|
server:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
|
||||||
"spaces": 2,
|
|
||||||
"generator-cli": {
|
|
||||||
"version": "7.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,69 +10,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.19.1",
|
"body-parser": "^1.19.1",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.17.2",
|
"express": "^4.17.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"ms": "^2.1.3",
|
|
||||||
"mysql": "^2.14.1",
|
"mysql": "^2.14.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
|
||||||
"swagger-ui-express": "^5.0.0",
|
|
||||||
"typeorm": "0.3.20"
|
"typeorm": "0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/ms": "^0.7.34",
|
|
||||||
"@types/node": "^16.11.10",
|
"@types/node": "^16.11.10",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "4.5.2"
|
"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": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
|
@ -126,11 +78,6 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@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": {
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
|
|
@ -216,15 +163,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
|
|
@ -255,11 +193,6 @@
|
||||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/jsonwebtoken": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
|
||||||
|
|
@ -320,22 +253,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
|
@ -480,11 +397,6 @@
|
||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
"devOptional": true
|
"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": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
|
@ -615,11 +527,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
|
@ -841,14 +748,6 @@
|
||||||
"color-support": "bin.js"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -896,18 +795,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
"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": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
|
@ -997,17 +884,6 @@
|
||||||
"node": ">=0.3.1"
|
"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": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.1",
|
"version": "16.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
|
||||||
|
|
@ -1063,14 +939,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
"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": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
|
@ -1555,17 +1423,6 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
|
@ -1606,11 +1463,6 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
|
@ -1621,11 +1473,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
"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": {
|
"node_modules/lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
|
@ -1646,11 +1493,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
"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": {
|
"node_modules/lodash.once": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
|
@ -1928,12 +1770,6 @@
|
||||||
"wrappy": "1"
|
"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": {
|
"node_modules/parse5": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||||
|
|
@ -2428,94 +2264,6 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/tar": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||||
|
|
@ -2826,14 +2574,6 @@
|
||||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
"devOptional": true
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|
@ -3014,14 +2754,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"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": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|
@ -3092,34 +2824,6 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,21 @@
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/ms": "^0.7.34",
|
"@types/ms": "^0.7.34",
|
||||||
"@types/node": "^16.11.10",
|
"@types/node": "^16.11.10",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "4.5.2"
|
"typescript": "4.5.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.19.1",
|
"body-parser": "^1.19.1",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.17.2",
|
"express": "^4.17.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"mysql": "^2.14.1",
|
"mysql": "^2.14.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
|
||||||
"swagger-ui-express": "^5.0.0",
|
|
||||||
"typeorm": "0.3.20"
|
"typeorm": "0.3.20"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,370 +1,53 @@
|
||||||
import { AppDataSource } from "../data-source"
|
import { AppDataSource } from "../data-source"
|
||||||
import { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response } from "express"
|
||||||
import { User } from "../entity/User"
|
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 {
|
export class UserController {
|
||||||
private userRepository = AppDataSource.getRepository(User);
|
|
||||||
|
|
||||||
private notificationRepository = AppDataSource.getRepository(Notification);
|
private userRepository = AppDataSource.getRepository(User)
|
||||||
/**
|
|
||||||
* @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();
|
|
||||||
|
|
||||||
// remove sensitive fields
|
async all(request: Request, response: Response, next: NextFunction) {
|
||||||
users.forEach((user) => {
|
return this.userRepository.find()
|
||||||
user.deleteSensitiveFields();
|
|
||||||
});
|
|
||||||
|
|
||||||
// remove the users set to private
|
|
||||||
const publicUsers = users.filter((user) => user.isPrivate === false);
|
|
||||||
|
|
||||||
res.status(200).send(publicUsers);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
async one(request: Request, response: Response, next: NextFunction) {
|
||||||
* @swagger
|
const id = parseInt(request.params.id)
|
||||||
* /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: AppRequest, 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: { likedBy: true },
|
|
||||||
comments: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return next(new AppError("No user found with that ID", 404));
|
const user = await this.userRepository.findOne({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
|
||||||
// remove sensitive fields
|
if (!user) {
|
||||||
user.deletePrivateFields();
|
return "unregistered user"
|
||||||
if (user.posts) {
|
}
|
||||||
user.posts.forEach((post) => {
|
return user
|
||||||
if (post.likedBy) {
|
|
||||||
post.likedBy.forEach((user) => user.deleteSensitiveFields());
|
|
||||||
}
|
|
||||||
if (post.comments) {
|
|
||||||
post.comments.forEach((comment) =>
|
|
||||||
comment.createdBy.deleteSensitiveFields()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (user.followed) {
|
|
||||||
user.followed.forEach((followedUser) =>
|
|
||||||
followedUser.deleteSensitiveFields()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let followStatus = false;
|
|
||||||
if (user.followers) {
|
|
||||||
user.followers.forEach((follower) => follower.deleteSensitiveFields());
|
|
||||||
// Set the followed status of the user
|
|
||||||
followStatus = user.followers.some(
|
|
||||||
(follower) => follower.id === req.user.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user is private, only return the user
|
|
||||||
// if the requesting user is following
|
|
||||||
|
|
||||||
if (user.isPrivate && !followStatus) {
|
|
||||||
user.followed = [];
|
|
||||||
user.followers = [];
|
|
||||||
user.posts = [];
|
|
||||||
user.comments = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(user);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
async save(request: Request, response: Response, next: NextFunction) {
|
||||||
* @swagger
|
const { firstName, lastName, age } = request.body;
|
||||||
* /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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
user.followed.forEach((followedUser) =>
|
const user = Object.assign(new User(), {
|
||||||
followedUser.deleteSensitiveFields()
|
firstName,
|
||||||
);
|
lastName,
|
||||||
user.followers.forEach((follower) => follower.deleteSensitiveFields());
|
age
|
||||||
|
})
|
||||||
|
|
||||||
return res.status(200).send(user);
|
return this.userRepository.save(user)
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
async remove(request: Request, response: Response, next: NextFunction) {
|
||||||
* @swagger
|
const id = parseInt(request.params.id)
|
||||||
* /users/me:
|
|
||||||
* patch:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags:
|
|
||||||
* - Users
|
|
||||||
* summary: Update the currently logged in user
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* isPrivate:
|
|
||||||
* type: boolean
|
|
||||||
* description: Whether the user's account is private
|
|
||||||
* profilePictureId:
|
|
||||||
* type: string
|
|
||||||
* description: The ID of the user's profile picture
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Successfully updated the user
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/UserWithRelationsAndNotifications'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid request body
|
|
||||||
*/
|
|
||||||
public updateMe = 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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Strict check, as we are dealing with a boolean value in this field
|
|
||||||
user.isPrivate =
|
|
||||||
req.body.isPrivate === undefined ? user.isPrivate : req.body.isPrivate;
|
|
||||||
user.profilePictureId =
|
|
||||||
req.body.profilePictureId || user.profilePictureId;
|
|
||||||
await this.userRepository.save(user);
|
|
||||||
|
|
||||||
return res.status(200).send(user);
|
let userToRemove = await this.userRepository.findOneBy({ id })
|
||||||
|
|
||||||
|
if (!userToRemove) {
|
||||||
|
return "this user not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.remove(userToRemove)
|
||||||
|
|
||||||
|
return "user has been removed"
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /users/follow/{id}:
|
|
||||||
* 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));
|
|
||||||
|
|
||||||
const user = req.user;
|
|
||||||
const userToFollow = await this.userRepository.findOne({
|
|
||||||
where: { id: parsedId },
|
|
||||||
relations: { followed: true, followers: true, notifications: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userToFollow)
|
|
||||||
return next(new AppError("No user found with that ID", 404));
|
|
||||||
|
|
||||||
// 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 res.status(200).send({
|
|
||||||
status: "success",
|
|
||||||
message: `You are now following ${userToFollow.firstName}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /users/follow/{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
|
|
||||||
*/
|
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
@ -38,48 +38,9 @@ export class AuthController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public handleSignUp = catchAsync(async (req, res, next) => {
|
||||||
* @swagger
|
const {email, password, passwordValidation, firstName, lastName} = req.body
|
||||||
* /auth/signup:
|
// Body Validation
|
||||||
* 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) {
|
if (password != passwordValidation) {
|
||||||
return next(new AppError('Passwords do not match', 400))
|
return next(new AppError('Passwords do not match', 400))
|
||||||
|
|
@ -103,53 +64,11 @@ export class AuthController {
|
||||||
this.sendToken(await this.userRepository.save(newUser), res)
|
this.sendToken(await this.userRepository.save(newUser), res)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
public handleLogin = catchAsync(async (req, res, next) => {
|
||||||
* @swagger
|
const {email, password, longExpiration} = req.body
|
||||||
* /auth/login:
|
if (!email || !password) {
|
||||||
* post:
|
return next(new AppError('Please provide email and password', 400))
|
||||||
* 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}})
|
const user = await this.userRepository.findOne({where: {email}})
|
||||||
if (!user || !(await user.comparePassword(password))) {
|
if (!user || !(await user.comparePassword(password))) {
|
||||||
|
|
@ -160,35 +79,14 @@ export class AuthController {
|
||||||
this.sendToken(user, res, {long: longExpiration})
|
this.sendToken(user, res, {long: longExpiration})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
public handleLogout = catchAsync(async (req: AppRequest, res, next) => {
|
||||||
* @swagger
|
// Set the jwt cookie to a dummy value and set the expiration to a date in the past
|
||||||
* /auth/logout:
|
res.cookie('jwt', 'loggedout', {
|
||||||
* get:
|
expires: new Date(Date.now() - 10000),
|
||||||
* security:
|
httpOnly: true
|
||||||
* - bearerAuth: []
|
})
|
||||||
* tags:
|
res.status(200).json({status: 'success'})
|
||||||
* - 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) => {
|
public protect = catchAsync(async (req: AppRequest, res, next) => {
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
|
|
@ -208,10 +106,7 @@ export class AuthController {
|
||||||
// Verify the token
|
// Verify the token
|
||||||
const decoded = jwt.verify(token, this.jwt_secret) as JwtPayload
|
const decoded = jwt.verify(token, this.jwt_secret) as JwtPayload
|
||||||
// Check if the user still exists
|
// Check if the user still exists
|
||||||
const candidateUser = await this.userRepository.findOne({
|
const candidateUser = await this.userRepository.findOne({where: {id: decoded.id}})
|
||||||
where: {id: decoded.id},
|
|
||||||
relations: {followed: true, followers: true, notifications: true}
|
|
||||||
})
|
|
||||||
if(!candidateUser){
|
if(!candidateUser){
|
||||||
return next(new AppError('The user belonging to this token no longer exists', 401))
|
return next(new AppError('The user belonging to this token no longer exists', 401))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,571 +0,0 @@
|
||||||
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: AppRequest, res, _next) => {
|
|
||||||
const posts = await this.postRepository.find({
|
|
||||||
relations: {
|
|
||||||
createdBy: true,
|
|
||||||
likedBy: true,
|
|
||||||
comments: { createdBy: true },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove sensitive fields
|
|
||||||
posts.forEach((post) => {
|
|
||||||
post.deleteSensitiveFields();
|
|
||||||
});
|
|
||||||
// Remove private, non followed users
|
|
||||||
const filteredPosts = posts.filter((post) => {
|
|
||||||
let followStatus = false;
|
|
||||||
|
|
||||||
// Get if the req user is in the follower list
|
|
||||||
if (post.createdBy.followers) {
|
|
||||||
followStatus = post.createdBy.followers.some(
|
|
||||||
(follower) => follower.id === req.user.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(post.createdBy.isPrivate && !followStatus);
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).send(filteredPosts);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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: { posts: { likedBy: true, comments: true } },
|
|
||||||
posts: {
|
|
||||||
likedBy: true,
|
|
||||||
comments: true,
|
|
||||||
createdBy: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all posts from the followed users
|
|
||||||
const followedPosts = user.followed
|
|
||||||
.map((followedUser) => {
|
|
||||||
followedUser.deleteSensitiveFields();
|
|
||||||
if (followedUser.posts) {
|
|
||||||
return followedUser.posts.map((post) => {
|
|
||||||
if (post.likedBy.length > 0) {
|
|
||||||
post.likedBy.forEach((user) => user.deleteSensitiveFields());
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...post,
|
|
||||||
createdBy: followedUser,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
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<validatedEntities> => {
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { DataSource } from "typeorm"
|
||||||
import { User } from "./entity/User"
|
import { User } from "./entity/User"
|
||||||
import {Comment} from "./entity/Comment";
|
import {Comment} from "./entity/Comment";
|
||||||
import {Post} from "./entity/Post";
|
import {Post} from "./entity/Post";
|
||||||
import {Notification} from "./entity/Notification";
|
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: "mysql",
|
type: "mysql",
|
||||||
|
|
@ -14,7 +13,7 @@ export const AppDataSource = new DataSource({
|
||||||
database: process.env.MYSQL_DATABASE,
|
database: process.env.MYSQL_DATABASE,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [User, Comment, Post, Notification],
|
entities: [User, Comment, Post],
|
||||||
migrations: [],
|
migrations: [],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
|
import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
|
||||||
import {User} from "./User";
|
import {User} from "./User";
|
||||||
import {Post} from "./Post";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Comment {
|
export class Comment {
|
||||||
|
|
@ -13,12 +12,9 @@ export class Comment {
|
||||||
@Column()
|
@Column()
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
@ManyToOne(() => User, user => user.comments)
|
@ManyToOne(() => User, user => user.posts)
|
||||||
createdBy: User
|
createdBy: User
|
||||||
|
|
||||||
@ManyToOne(() => Post, post => post.comments)
|
|
||||||
post: Post
|
|
||||||
|
|
||||||
@ManyToMany(()=> User)
|
@ManyToMany(()=> User)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
likedBy: User[]
|
likedBy: User[]
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import {Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn} from "typeorm";
|
import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
|
||||||
import {User} from "./User";
|
import {User} from "./User";
|
||||||
import {Comment} from "./Comment";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Post {
|
export class Post {
|
||||||
|
|
@ -19,28 +18,8 @@ export class Post {
|
||||||
@ManyToOne(() => User, user => user.posts)
|
@ManyToOne(() => User, user => user.posts)
|
||||||
createdBy: User
|
createdBy: User
|
||||||
|
|
||||||
@OneToMany(() => Comment, comment => comment.post)
|
|
||||||
comments: Comment[]
|
|
||||||
|
|
||||||
@ManyToMany(()=> User)
|
@ManyToMany(()=> User)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
likedBy: User[]
|
likedBy: User[]
|
||||||
|
|
||||||
public deleteSensitiveFields(){
|
|
||||||
this.createdBy.deleteSensitiveFields()
|
|
||||||
if (this.likedBy) {
|
|
||||||
if (this.likedBy.length > 0) {
|
|
||||||
this.likedBy.forEach((user) => user.deleteSensitiveFields());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.comments) {
|
|
||||||
if (this.comments.length > 0) {
|
|
||||||
this.comments.forEach((comment) =>
|
|
||||||
comment.createdBy.deleteSensitiveFields()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,67 +2,41 @@ import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, OneToMany, JoinTable
|
||||||
import * as bcrypt from "bcrypt"
|
import * as bcrypt from "bcrypt"
|
||||||
import {Post} from "./Post";
|
import {Post} from "./Post";
|
||||||
import {Comment} from "./Comment";
|
import {Comment} from "./Comment";
|
||||||
import {Notification} from "./Notification";
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column()
|
@PrimaryGeneratedColumn()
|
||||||
firstName: string;
|
id: number
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
lastName: string;
|
firstName: string
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
email: string;
|
lastName: string
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
password: string;
|
email: string
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column()
|
||||||
isPrivate: boolean;
|
password: string
|
||||||
|
|
||||||
@Column({ default: null })
|
@ManyToMany(type => User)
|
||||||
profilePictureId: string;
|
@JoinTable()
|
||||||
|
followed: User[]
|
||||||
|
|
||||||
@ManyToMany((type) => User)
|
@OneToMany(type => Post, post=> post.createdBy)
|
||||||
@JoinTable()
|
posts: Post[]
|
||||||
followed: User[];
|
|
||||||
|
|
||||||
@ManyToMany((type) => User)
|
@OneToMany(type => Comment, comment=> comment.createdBy)
|
||||||
@JoinTable()
|
comments: Comment[]
|
||||||
followers: User[];
|
|
||||||
|
|
||||||
@OneToMany((type) => Post, (post) => post.createdBy)
|
|
||||||
posts: Post[];
|
|
||||||
|
|
||||||
@OneToMany((type) => Comment, (comment) => comment.createdBy)
|
static async hashPassword(password: string){
|
||||||
comments: Comment[];
|
return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS))
|
||||||
|
}
|
||||||
|
|
||||||
@OneToMany((type) => Notification, (notification) => notification.belongsTo)
|
async comparePassword(password: string){
|
||||||
notifications: Notification[];
|
return await bcrypt.compare(password, this.password)
|
||||||
|
}
|
||||||
static async hashPassword(password: string) {
|
|
||||||
return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS));
|
|
||||||
}
|
|
||||||
|
|
||||||
async comparePassword(password: string) {
|
|
||||||
return await bcrypt.compare(password, this.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteSensitiveFields() {
|
|
||||||
this.deleteExtraFields();
|
|
||||||
this.deletePrivateFields();
|
|
||||||
}
|
|
||||||
public deleteExtraFields() {
|
|
||||||
delete this.followed;
|
|
||||||
delete this.followers;
|
|
||||||
}
|
|
||||||
public deletePrivateFields(){
|
|
||||||
delete this.email;
|
|
||||||
delete this.password;
|
|
||||||
delete this.notifications;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,24 @@ import * as bodyParser from "body-parser"
|
||||||
import { AppDataSource } from "./data-source"
|
import { AppDataSource } from "./data-source"
|
||||||
import {errorHandler} from "./controller/errorController";
|
import {errorHandler} from "./controller/errorController";
|
||||||
import {AuthRoutes} from "./routes/authRoutes";
|
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()
|
AppDataSource.initialize().then(async () => {
|
||||||
.then(async () => {
|
|
||||||
// create express app
|
// create express app
|
||||||
const app = express();
|
const app = express()
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json())
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
// register express routes from defined application routes
|
// register express routes from defined application routes
|
||||||
// Auth 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
|
// setup express app here
|
||||||
app.use(errorHandler);
|
app.use(errorHandler)
|
||||||
// start express server
|
// 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"
|
|
||||||
);
|
console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results")
|
||||||
})
|
|
||||||
.catch((error) => console.log(error));
|
}).catch(error => console.log(error))
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export const AuthRoutes = Router();
|
||||||
|
|
||||||
AuthRoutes.route("/signup").post(authController.handleSignUp)
|
AuthRoutes.route("/signup").post(authController.handleSignUp)
|
||||||
AuthRoutes.route("/login").post(authController.handleLogin)
|
AuthRoutes.route("/login").post(authController.handleLogin)
|
||||||
AuthRoutes.route("/logout").get(authController.handleLogout)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
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",
|
|
||||||
},
|
|
||||||
isPrivate: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "User private status",
|
|
||||||
},
|
|
||||||
profilePictureId: {
|
|
||||||
type: "string",
|
|
||||||
description: "User profile picture ID",
|
|
||||||
},
|
|
||||||
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));
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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)
|
|
||||||
.patch(userController.updateMe);
|
|
||||||
UserRoutes.route("/:id").get(userController.getUser)
|
|
||||||
UserRoutes.route("/follow/:id")
|
|
||||||
.post(userController.followUser)
|
|
||||||
.delete(userController.unfollowUser)
|
|
||||||
Loading…
Reference in New Issue