Compare commits

..

No commits in common. "e6f73932faccd4fca040792dc946d7314a230efc" and "be57bd32b8ac8e8fbbd7f3270f4c702340bd1b5f" have entirely different histories.

12 changed files with 138 additions and 609 deletions

View File

@ -108,14 +108,6 @@
"type": "string", "type": "string",
"description": "User first name" "description": "User first name"
}, },
"isPrivate": {
"type": "boolean",
"description": "User private status"
},
"profilePictureId": {
"type": "string",
"description": "User profile picture ID"
},
"lastName": { "lastName": {
"type": "string", "type": "string",
"description": "User last name" "description": "User last name"
@ -777,7 +769,9 @@
}, },
"/users/me": { "/users/me": {
"get": { "get": {
"tags": ["Users"], "tags": [
"Users"
],
"summary": "Get the currently logged in user", "summary": "Get the currently logged in user",
"security": [ "security": [
{ {
@ -796,50 +790,6 @@
} }
} }
} }
},
"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": { "/users/{id}/follow": {

View File

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

View File

@ -284,18 +284,6 @@ export interface User {
* @memberof User * @memberof User
*/ */
'firstName'?: string; 'firstName'?: string;
/**
* User private status
* @type {boolean}
* @memberof User
*/
'isPrivate'?: boolean;
/**
* User profile picture ID
* @type {string}
* @memberof User
*/
'profilePictureId'?: string;
/** /**
* User last name * User last name
* @type {string} * @type {string}
@ -321,18 +309,6 @@ export interface UserWithRelations {
* @memberof UserWithRelations * @memberof UserWithRelations
*/ */
'firstName'?: string; 'firstName'?: string;
/**
* User private status
* @type {boolean}
* @memberof UserWithRelations
*/
'isPrivate'?: boolean;
/**
* User profile picture ID
* @type {string}
* @memberof UserWithRelations
*/
'profilePictureId'?: string;
/** /**
* User last name * User last name
* @type {string} * @type {string}
@ -382,18 +358,6 @@ export interface UserWithRelationsAndNotifications {
* @memberof UserWithRelationsAndNotifications * @memberof UserWithRelationsAndNotifications
*/ */
'firstName'?: string; 'firstName'?: string;
/**
* User private status
* @type {boolean}
* @memberof UserWithRelationsAndNotifications
*/
'isPrivate'?: boolean;
/**
* User profile picture ID
* @type {string}
* @memberof UserWithRelationsAndNotifications
*/
'profilePictureId'?: string;
/** /**
* User last name * User last name
* @type {string} * @type {string}
@ -431,25 +395,6 @@ export interface UserWithRelationsAndNotifications {
*/ */
'notifications'?: Array<Notification>; '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 * AuthenticationApi - axios parameter creator
@ -1583,46 +1528,6 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.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 { return {
url: toPathString(localVarUrlObj), url: toPathString(localVarUrlObj),
options: localVarRequestOptions, options: localVarRequestOptions,
@ -1701,19 +1606,6 @@ export const UsersApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url; const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); 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);
},
} }
}; };
@ -1772,16 +1664,6 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
usersMeGet(options?: any): AxiosPromise<UserWithRelationsAndNotifications> { usersMeGet(options?: any): AxiosPromise<UserWithRelationsAndNotifications> {
return localVarFp.usersMeGet(options).then((request) => request(axios, basePath)); 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));
},
}; };
}; };
@ -1849,18 +1731,6 @@ export class UsersApi extends BaseAPI {
public usersMeGet(options?: RawAxiosRequestConfig) { public usersMeGet(options?: RawAxiosRequestConfig) {
return UsersApiFp(this.configuration).usersMeGet(options).then((request) => request(this.axios, this.basePath)); 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

@ -2,13 +2,11 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import loginReducer from "./loginSlice"; import loginReducer from "./loginSlice";
import postReducer from "./postSlice"; import postReducer from "./postSlice";
import usersReducer from "./usersSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
login: loginReducer, login: loginReducer,
post: postReducer, post: postReducer,
users: usersReducer,
}, },
}); });

View File

@ -1,116 +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));
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 {
await api.usersIdFollowPost(userId);
dispatch(getUsers());
} catch (error) {
dispatch(setError((error as Error).message));
dispatch(setStatus(Status.idle));
}
};
export const unFollowUser =
(userId: number): AppThunk =>
async (dispatch) => {
const api = createApi(store);
dispatch(setStatus(Status.loading));
try {
await api.usersIdFollowDelete(userId);
dispatch(getUsers());
} catch (error) {
dispatch(setError((error as Error).message));
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);
}

View File

@ -1,15 +0,0 @@
import { Avatar } from "@mui/material";
import { User, UserWithRelations } from "../api";
interface AppAvatarProps {
user: User | UserWithRelations;
}
export function AppAvatar(props: AppAvatarProps) {
return (
<Avatar
alt={`${props.user.firstName} ${props.user.lastName}`}
src={`/images/${props.user.profilePictureId}`}
/>
);
}

View File

@ -5,13 +5,13 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import Avatar from "@mui/material/Avatar";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import NotificationBell from "./notificationBell"; import NotificationBell from "./notificationBell";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import { postLogout, selectUserInfo } from "../app/loginSlice"; import { postLogout, selectUserInfo } from "../app/loginSlice";
import { useAppDispatch } from "../app/store"; import { useAppDispatch } from "../app/store";
import { AppAvatar } from "./appAvatar";
interface TopAppBarProps { interface TopAppBarProps {
height: number; height: number;
@ -39,10 +39,7 @@ function TopAppBar(props: TopAppBarProps) {
}; };
return ( return (
<AppBar <AppBar position="absolute" sx={{zIndex:1600, height: `${props.height}px` }}>
position="absolute"
sx={{ zIndex: 1600, height: `${props.height}px` }}
>
<Toolbar disableGutters> <Toolbar disableGutters>
<Typography <Typography
variant="h6" variant="h6"
@ -75,7 +72,10 @@ function TopAppBar(props: TopAppBarProps) {
<Box sx={{ flexGrow: 0, mr:"2rem" }}> <Box sx={{ flexGrow: 0, mr:"2rem" }}>
<Tooltip title="Open settings" > <Tooltip title="Open settings" >
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<AppAvatar user={userInfo} /> <Avatar
alt={`${userInfo.firstName} ${userInfo.lastName}`}
src="/static/images/avatar/2.jpg"
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Menu <Menu

View File

@ -1,79 +1,3 @@
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() { export default function Search() {
const userList = useSelector(selectUsers); return <>Search</>;
const [displayUserList, setDisplayUserList] = React.useState(userList);
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
dispatch(getUsers());
}, []);
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;
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) => (
<>
<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" }} />
</>
))}
</List>
</Paper>
</Box>
);
} }

View File

@ -38,10 +38,7 @@ export class UserController {
user.deleteSensitiveFields(); user.deleteSensitiveFields();
}); });
// remove the users set to private res.status(200).send(users);
const publicUsers = users.filter((user) => user.isPrivate === false);
res.status(200).send(publicUsers);
} }
); );
@ -70,7 +67,7 @@ export class UserController {
* $ref: '#/components/schemas/UserWithRelations' * $ref: '#/components/schemas/UserWithRelations'
*/ */
public getUser = catchAsync( public getUser = catchAsync(
async (req: AppRequest, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const id = req.params.id; const id = req.params.id;
const parsedId = parseInt(id); const parsedId = parseInt(id);
// Check if ID is a number // Check if ID is a number
@ -95,18 +92,6 @@ export class UserController {
); );
user.followers.forEach((follower) => follower.deleteSensitiveFields()); 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); return res.send(user);
} }
); );
@ -150,60 +135,6 @@ 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 * @swagger
* /users/{id}/follow: * /users/{id}/follow:

View File

@ -6,57 +6,53 @@ import {Notification} from "./Notification";
@Entity() @Entity()
export class User { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number
@Column() @Column()
firstName: string; firstName: string
@Column() @Column()
lastName: string; lastName: string
@Column() @Column()
email: string; email: string
@Column() @Column()
password: string; password: string
@Column({ default: false }) @ManyToMany(type => User)
isPrivate: boolean;
@Column({ default: null })
profilePictureId: string;
@ManyToMany((type) => User)
@JoinTable() @JoinTable()
followed: User[]; followed: User[]
@ManyToMany((type) => User) @ManyToMany(type => User)
@JoinTable() @JoinTable()
followers: User[]; followers: User[]
@OneToMany((type) => Post, (post) => post.createdBy) @OneToMany(type => Post, post=> post.createdBy)
posts: Post[]; posts: Post[]
@OneToMany((type) => Comment, (comment) => comment.createdBy) @OneToMany(type => Comment, comment=> comment.createdBy)
comments: Comment[]; comments: Comment[]
@OneToMany(type => Notification, notification => notification.belongsTo)
notifications: Notification[]
@OneToMany((type) => Notification, (notification) => notification.belongsTo)
notifications: Notification[];
static async hashPassword(password: string){ static async hashPassword(password: string){
return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS)); return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS))
} }
async comparePassword(password: string){ async comparePassword(password: string){
return await bcrypt.compare(password, this.password); return await bcrypt.compare(password, this.password)
} }
public deleteSensitiveFields(){ public deleteSensitiveFields(){
delete this.password; delete this.password
delete this.email; delete this.email
delete this.notifications; delete this.notifications
delete this.followed; delete this.followed
delete this.followers; delete this.followers
} }
} }

View File

@ -115,14 +115,6 @@ const swaggerOptions = {
type: "string", type: "string",
description: "User first name", description: "User first name",
}, },
isPrivate: {
type: "boolean",
description: "User private status",
},
profilePictureId: {
type: "string",
description: "User profile picture ID",
},
lastName: { lastName: {
type: "string", type: "string",
description: "User last name", description: "User last name",

View File

@ -6,9 +6,7 @@ export const UserRoutes = Router();
const userController = new UserController() const userController = new UserController()
UserRoutes.route("/").get(userController.getAllUsers) UserRoutes.route("/").get(userController.getAllUsers)
UserRoutes.route("/me") UserRoutes.route("/me").get(userController.getMe)
.get(userController.getMe)
.patch(userController.updateMe);
UserRoutes.route("/:id").get(userController.getUser) UserRoutes.route("/:id").get(userController.getUser)
UserRoutes.route("/follow/:id") UserRoutes.route("/follow/:id")
.post(userController.followUser) .post(userController.followUser)