From fda05c661a01e93ba070171ca97805b99ad1466e Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sat, 10 Feb 2024 15:24:47 +0100 Subject: [PATCH] :card_file_box: Private user setup, and preparations for images upload Signed-off-by: Pau Costa --- DevSpaceOApi.json | 114 +++++++++++++++------ client/src/api/.openapi-generator/FILES | 1 - client/src/api/api.ts | 130 ++++++++++++++++++++++++ server/src/controller/UserController.ts | 73 ++++++++++++- server/src/entity/User.ts | 74 +++++++------- server/src/routes/swaggerRoutes.ts | 8 ++ server/src/routes/userRoutes.ts | 4 +- 7 files changed, 333 insertions(+), 71 deletions(-) diff --git a/DevSpaceOApi.json b/DevSpaceOApi.json index 251138e..7f33f13 100644 --- a/DevSpaceOApi.json +++ b/DevSpaceOApi.json @@ -100,18 +100,26 @@ "User": { "type": "object", "properties": { - "id": { - "type": "integer", - "description": "User ID" - }, - "firstName": { - "type": "string", - "description": "User first name" - }, - "lastName": { - "type": "string", - "description": "User last name" - } + "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": { @@ -768,29 +776,71 @@ } }, "/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" - } - } - } + "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/{id}/follow": { "post": { diff --git a/client/src/api/.openapi-generator/FILES b/client/src/api/.openapi-generator/FILES index 16b445e..a80cd4f 100644 --- a/client/src/api/.openapi-generator/FILES +++ b/client/src/api/.openapi-generator/FILES @@ -1,6 +1,5 @@ .gitignore .npmignore -.openapi-generator-ignore api.ts base.ts common.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 4c5df5b..1291ec4 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -284,6 +284,18 @@ export interface User { * @memberof User */ 'firstName'?: string; + /** + * User private status + * @type {boolean} + * @memberof User + */ + 'isPrivate'?: boolean; + /** + * User profile picture ID + * @type {string} + * @memberof User + */ + 'profilePictureId'?: string; /** * User last name * @type {string} @@ -309,6 +321,18 @@ export interface UserWithRelations { * @memberof UserWithRelations */ 'firstName'?: string; + /** + * User private status + * @type {boolean} + * @memberof UserWithRelations + */ + 'isPrivate'?: boolean; + /** + * User profile picture ID + * @type {string} + * @memberof UserWithRelations + */ + 'profilePictureId'?: string; /** * User last name * @type {string} @@ -358,6 +382,18 @@ export interface UserWithRelationsAndNotifications { * @memberof UserWithRelationsAndNotifications */ 'firstName'?: string; + /** + * User private status + * @type {boolean} + * @memberof UserWithRelationsAndNotifications + */ + 'isPrivate'?: boolean; + /** + * User profile picture ID + * @type {string} + * @memberof UserWithRelationsAndNotifications + */ + 'profilePictureId'?: string; /** * User last name * @type {string} @@ -395,6 +431,25 @@ export interface UserWithRelationsAndNotifications { */ 'notifications'?: Array; } +/** + * + * @export + * @interface UsersMePatchRequest + */ +export interface UsersMePatchRequest { + /** + * Whether the user\'s account is private + * @type {boolean} + * @memberof UsersMePatchRequest + */ + 'isPrivate'?: boolean; + /** + * The ID of the user\'s profile picture + * @type {string} + * @memberof UsersMePatchRequest + */ + 'profilePictureId'?: string; +} /** * AuthenticationApi - axios parameter creator @@ -1528,6 +1583,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Update the currently logged in user + * @param {UsersMePatchRequest} usersMePatchRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersMePatch: async (usersMePatchRequest: UsersMePatchRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'usersMePatchRequest' is not null or undefined + assertParamExists('usersMePatch', 'usersMePatchRequest', usersMePatchRequest) + const localVarPath = `/users/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(usersMePatchRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -1606,6 +1701,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * + * @summary Update the currently logged in user + * @param {UsersMePatchRequest} usersMePatchRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersMePatch(usersMePatchRequest: UsersMePatchRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersMePatch(usersMePatchRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMePatch']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, } }; @@ -1664,6 +1772,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath usersMeGet(options?: any): AxiosPromise { return localVarFp.usersMeGet(options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Update the currently logged in user + * @param {UsersMePatchRequest} usersMePatchRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersMePatch(usersMePatchRequest: UsersMePatchRequest, options?: any): AxiosPromise { + return localVarFp.usersMePatch(usersMePatchRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -1731,6 +1849,18 @@ export class UsersApi extends BaseAPI { public usersMeGet(options?: RawAxiosRequestConfig) { return UsersApiFp(this.configuration).usersMeGet(options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @summary Update the currently logged in user + * @param {UsersMePatchRequest} usersMePatchRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersMePatch(usersMePatchRequest: UsersMePatchRequest, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersMePatch(usersMePatchRequest, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index 0445823..5415762 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -38,7 +38,10 @@ export class UserController { user.deleteSensitiveFields(); }); - res.status(200).send(users); + // remove the users set to private + const publicUsers = users.filter((user) => user.isPrivate === false); + + res.status(200).send(publicUsers); } ); @@ -67,7 +70,7 @@ export class UserController { * $ref: '#/components/schemas/UserWithRelations' */ public getUser = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { + async (req: AppRequest, res: Response, next: NextFunction) => { const id = req.params.id; const parsedId = parseInt(id); // Check if ID is a number @@ -92,6 +95,18 @@ export class UserController { ); user.followers.forEach((follower) => follower.deleteSensitiveFields()); + // If the user is private, only return the user + // if the requesting user is following + const followStatus = user.followers.some( + (follower) => follower.id === req.user.id + ); + if (user.isPrivate && !followStatus) { + user.followed = []; + user.followers = []; + user.posts = []; + user.comments = []; + } + return res.send(user); } ); @@ -135,6 +150,60 @@ export class UserController { } ); + /** + * @swagger + * /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, + }, + }); + + user.isPrivate = req.body.isPrivate || user.isPrivate; + user.profilePictureId = + req.body.profilePictureId || user.profilePictureId; + await this.userRepository.save(user); + + return res.status(200).send(user); + } + ); + /** * @swagger * /users/{id}/follow: diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index d2b9705..91e1866 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -6,53 +6,57 @@ import {Notification} from "./Notification"; @Entity() export class User { + @PrimaryGeneratedColumn() + id: number; - @PrimaryGeneratedColumn() - id: number + @Column() + firstName: string; - @Column() - firstName: string + @Column() + lastName: string; - @Column() - lastName: string + @Column() + email: string; - @Column() - email: string + @Column() + password: string; - @Column() - password: string + @Column({ default: true }) + isPrivate: boolean; - @ManyToMany(type => User) - @JoinTable() - followed: User[] + @Column({ default: null }) + profilePictureId: string; - @ManyToMany(type => User) - @JoinTable() - followers: User[] + @ManyToMany((type) => User) + @JoinTable() + followed: User[]; - @OneToMany(type => Post, post=> post.createdBy) - posts: Post[] + @ManyToMany((type) => User) + @JoinTable() + followers: User[]; - @OneToMany(type => Comment, comment=> comment.createdBy) - comments: Comment[] + @OneToMany((type) => Post, (post) => post.createdBy) + posts: Post[]; - @OneToMany(type => Notification, notification => notification.belongsTo) - notifications: Notification[] + @OneToMany((type) => Comment, (comment) => comment.createdBy) + comments: Comment[]; + @OneToMany((type) => Notification, (notification) => notification.belongsTo) + notifications: Notification[]; - static async hashPassword(password: string){ - return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS)) - } + 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) - } + async comparePassword(password: string) { + return await bcrypt.compare(password, this.password); + } - public deleteSensitiveFields(){ - delete this.password - delete this.email - delete this.notifications - delete this.followed - delete this.followers - } + public deleteSensitiveFields() { + delete this.password; + delete this.email; + delete this.notifications; + delete this.followed; + delete this.followers; + } } diff --git a/server/src/routes/swaggerRoutes.ts b/server/src/routes/swaggerRoutes.ts index 152055e..839a455 100644 --- a/server/src/routes/swaggerRoutes.ts +++ b/server/src/routes/swaggerRoutes.ts @@ -115,6 +115,14 @@ const swaggerOptions = { 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", diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts index c172de0..ed2d768 100644 --- a/server/src/routes/userRoutes.ts +++ b/server/src/routes/userRoutes.ts @@ -6,7 +6,9 @@ export const UserRoutes = Router(); const userController = new UserController() UserRoutes.route("/").get(userController.getAllUsers) -UserRoutes.route("/me").get(userController.getMe) +UserRoutes.route("/me") + .get(userController.getMe) + .patch(userController.updateMe); UserRoutes.route("/:id").get(userController.getUser) UserRoutes.route("/follow/:id") .post(userController.followUser)