🗃️ Private user setup, and preparations for images upload

Signed-off-by: Pau Costa <mico@micodev.es>
main
Pau Costa Ferrer 2024-02-10 15:24:47 +01:00
parent be57bd32b8
commit fda05c661a
7 changed files with 333 additions and 71 deletions

View File

@ -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": {

View File

@ -1,6 +1,5 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts

View File

@ -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<Notification>;
}
/**
*
* @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<RequestArgs> => {
// 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<UserWithRelationsAndNotifications>> {
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<UserWithRelationsAndNotifications> {
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<UserWithRelationsAndNotifications> {
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));
}
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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)