Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e11af3ad2a | |||
| d0ea12ff8c | |||
| 3ba8c2abd5 | |||
| 5a2b291bdd | |||
| 54da6e1bf5 | |||
| 27c1741bc0 | |||
| da43bec842 | |||
| 1b88aa7e56 | |||
| 04b51ae321 | |||
| bcbffad32d | |||
| 46f6280152 |
+4
-1
@@ -1,3 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
App.css
|
|
||||||
start_in_tmux.sh
|
start_in_tmux.sh
|
||||||
|
default.json
|
||||||
|
production.json
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Dev connector
|
||||||
|
---
|
||||||
|
> Web app created with React, Redux, MUI in the front end. Express and mongoose
|
||||||
|
> on the back end. It also has docker images.
|
||||||
|
|
||||||
|
## Docker Usage:
|
||||||
|
`docker compose -f docker-compose.yml up`
|
||||||
|
> The app is available at http://localhost:8080
|
||||||
|
|
||||||
|
## Run the app locally
|
||||||
|
Requires running mongodb service.
|
||||||
|
|
||||||
|
In the client directory:
|
||||||
|
`npm run dev`
|
||||||
|
In the server directory:
|
||||||
|
```MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database JWT_SECRET=mysecrettoken npm run server```
|
||||||
|
|
||||||
|
The app will be available at http://localhost:5173
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# The first FROM is now a stage called build-stage
|
|
||||||
|
|
||||||
FROM node:20 AS build-stage
|
FROM node:20 AS build-stage
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
image: devcon-frontend-dev
|
|
||||||
build:
|
|
||||||
context: . # The context will pick this directory as the "build context"
|
|
||||||
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
|
|
||||||
volumes:
|
|
||||||
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
|
|
||||||
ports:
|
|
||||||
- 5173:3000
|
|
||||||
container_name: devcon-front-dev
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
image: todo-front-dev
|
|
||||||
build:
|
|
||||||
context: . # The context will pick this directory as the "build context"
|
|
||||||
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
|
|
||||||
volumes:
|
|
||||||
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
|
|
||||||
ports:
|
|
||||||
- 5173:5173
|
|
||||||
container_name: todo-front
|
|
||||||
Generated
+15
-8
@@ -13,7 +13,7 @@
|
|||||||
"@mui/icons-material": "^6.1.1",
|
"@mui/icons-material": "^6.1.1",
|
||||||
"@mui/material": "^6.1.1",
|
"@mui/material": "^6.1.1",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@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/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"@babel/helper-validator-option": "^7.24.8",
|
"@babel/helper-validator-option": "^7.24.8",
|
||||||
"browserslist": "^4.23.1",
|
"browserslist": "^4.23.1",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"semver": "^6.3.0"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -175,6 +175,9 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-plugin-utils": {
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
@@ -1656,10 +1659,10 @@
|
|||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@types/aria-query": "^5.0.1",
|
"@types/aria-query": "^5.0.1",
|
||||||
"aria-query": "^5.0.0",
|
"aria-query": "5.1.3",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"dom-accessibility-api": "^0.5.9",
|
"dom-accessibility-api": "^0.5.9",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.5.0",
|
||||||
"pretty-format": "^27.0.2"
|
"pretty-format": "^27.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1809,7 +1812,7 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
@@ -1835,8 +1838,8 @@
|
|||||||
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
|
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expect": "^29.0.0",
|
"jest-matcher-utils": "^27.0.0",
|
||||||
"pretty-format": "^29.0.0"
|
"pretty-format": "^27.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
@@ -2027,6 +2030,10 @@
|
|||||||
{
|
{
|
||||||
"type": "tidelift",
|
"type": "tidelift",
|
||||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3527,7 +3534,7 @@
|
|||||||
"react": "^16.8 || ^17.0 || ^18.0",
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0",
|
"react-dom": "^16.8 || ^17.0 || ^18.0",
|
||||||
"react-native": ">=0.59",
|
"react-native": ">=0.59",
|
||||||
"redux": "^4"
|
"redux": "^4 || ^5.0.0-beta.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Navbar from './components/layout/Navbar';
|
|
||||||
import Landing from './components/layout/Landing';
|
|
||||||
import Register from './components/auth/Register';
|
|
||||||
import Login from './components/auth/Login';
|
|
||||||
import Alert from './components/layout/Alert';
|
|
||||||
import Dashboard from './components/dashboard/Dashboard';
|
|
||||||
import ProfileForm from './components/profile-forms/ProfileForm';
|
|
||||||
import AddExperience from './components/profile-forms/AddExperience';
|
|
||||||
import AddEducation from './components/profile-forms/AddEducation';
|
|
||||||
import Profiles from './components/profiles/Profiles';
|
|
||||||
import Profile from './components/profile/Profile';
|
|
||||||
import Posts from './components/posts/Posts';
|
|
||||||
import Post from './components/post/Post';
|
|
||||||
import NotFound from './components/layout/NotFound';
|
|
||||||
import PrivateRoute from './components/routing/PrivateRoute';
|
|
||||||
|
|
||||||
// Redux
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import store from './store';
|
|
||||||
import { loadUser } from './actions/auth';
|
|
||||||
import setAuthToken from './utils/setAuthToken';
|
|
||||||
|
|
||||||
import './App.css';
|
|
||||||
import { logOut } from './reducers/auth';
|
|
||||||
|
|
||||||
// Level - 1
|
|
||||||
//
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
useEffect(() => {
|
|
||||||
// check for token in LS when app first runs
|
|
||||||
if (localStorage.token) {
|
|
||||||
// if there is a token set axios headers for all requests
|
|
||||||
setAuthToken(localStorage.token);
|
|
||||||
}
|
|
||||||
// try to fetch a user, if no token or invalid token we
|
|
||||||
// will get a 401 response from our API
|
|
||||||
store.dispatch(loadUser());
|
|
||||||
|
|
||||||
// log user out from all tabs if they log out in one tab
|
|
||||||
window.addEventListener("storage", () => {
|
|
||||||
if (!localStorage.token) store.dispatch(logOut);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<Router>
|
|
||||||
<Navbar />
|
|
||||||
<Alert />
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Landing />} />
|
|
||||||
<Route path="register" element={<Register />} />
|
|
||||||
<Route path="login" element={<Login />} />
|
|
||||||
<Route path="profiles" element={<Profiles />} />
|
|
||||||
<Route path="profile/:id" element={<Profile />} />
|
|
||||||
<Route
|
|
||||||
path="dashboard"
|
|
||||||
element={<PrivateRoute component={Dashboard} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="create-profile"
|
|
||||||
element={<PrivateRoute component={ProfileForm} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="edit-profile"
|
|
||||||
element={<PrivateRoute component={ProfileForm} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="add-experience"
|
|
||||||
element={<PrivateRoute component={AddExperience} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="add-education"
|
|
||||||
element={<PrivateRoute component={AddEducation} />}
|
|
||||||
/>
|
|
||||||
<Route path="posts" element={<PrivateRoute component={Posts} />} />
|
|
||||||
<Route path="posts/:id" element={<PrivateRoute component={Post} />} />
|
|
||||||
<Route path="/*" element={<NotFound />} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
/*
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<Router>
|
|
||||||
<Routes>
|
|
||||||
<Route path="login" element={<Login />} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</Provider>
|
|
||||||
);*/
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
1. When creating profile in the Profile form need to be added validation for the
|
|
||||||
email because if the email is not email it gets sent to the server and crashes
|
|
||||||
Fix all the form data apis, formData
|
|
||||||
Extract the inpudate component from addeducation
|
|
||||||
maybe fix that addeducation
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import {removeAlert, setAlert } from '../reducers/alert';
|
|
||||||
|
|
||||||
export const createAlert = (msg, alertType, timeout = 5000) => dispatch => {
|
|
||||||
const id = uuidv4();
|
|
||||||
dispatch(setAlert({ msg, alertType, id }));
|
|
||||||
|
|
||||||
setTimeout(() => dispatch(removeAlert(id)), timeout);
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import api from "../utils/api";
|
|
||||||
import { createAlert } from "./alert";
|
|
||||||
import {
|
|
||||||
loginSucces,
|
|
||||||
authError,
|
|
||||||
registerSuccess,
|
|
||||||
logOut,
|
|
||||||
userLoaded,
|
|
||||||
} from "../reducers/auth";
|
|
||||||
/*
|
|
||||||
NOTE: we don't need a config object for axios as the
|
|
||||||
default headers in axios are already Content-Type: application/json
|
|
||||||
also axios stringifies and parses JSON for you, so no need for
|
|
||||||
JSON.stringify or JSON.parse
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const login = (email, password) => async (dispatch) => {
|
|
||||||
const body = { email, password };
|
|
||||||
try {
|
|
||||||
const res = await api.post("/auth", body);
|
|
||||||
|
|
||||||
dispatch(loginSucces(res.data));
|
|
||||||
|
|
||||||
dispatch(loadUser());
|
|
||||||
} catch (err) {
|
|
||||||
const errors = err.response.data.errors;
|
|
||||||
if (errors) {
|
|
||||||
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: "auth/loginFail",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadUser = () => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get("/auth");
|
|
||||||
|
|
||||||
dispatch(userLoaded(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(authError());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register User
|
|
||||||
export const register = (formData) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.post("/users", formData);
|
|
||||||
|
|
||||||
dispatch(registerSuccess(res.data));
|
|
||||||
dispatch(loadUser());
|
|
||||||
} catch (err) {
|
|
||||||
const errors = err.response.data.errors;
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "auth/registerFail",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logout = () => async (dispatch) => {
|
|
||||||
dispatch(logOut);
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import {
|
|
||||||
removeComment,
|
|
||||||
addCommentAction,
|
|
||||||
updateLikes,
|
|
||||||
postError,
|
|
||||||
deletePostAction,
|
|
||||||
getPostAction,
|
|
||||||
getPostsAction,
|
|
||||||
addPostAction,
|
|
||||||
} from "../reducers/post";
|
|
||||||
import api from "../utils/api";
|
|
||||||
import { createAlert } from "./alert";
|
|
||||||
|
|
||||||
// Get posts
|
|
||||||
export const getPosts = () => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get("/posts");
|
|
||||||
|
|
||||||
dispatch(getPostsAction(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add like
|
|
||||||
export const addLike = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.put(`/posts/like/${id}`);
|
|
||||||
|
|
||||||
dispatch(updateLikes({ id, likes: res.data }));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove like
|
|
||||||
export const removeLike = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.put(`/posts/unlike/${id}`);
|
|
||||||
|
|
||||||
dispatch(updateLikes({ id, likes: res.data }));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete post
|
|
||||||
export const deletePost = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
await api.delete(`/posts/${id}`);
|
|
||||||
|
|
||||||
dispatch(deletePostAction(id));
|
|
||||||
|
|
||||||
dispatch(createAlert("Post Removed", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add post
|
|
||||||
export const addPost = (formData) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.post("/posts", formData);
|
|
||||||
|
|
||||||
dispatch(addPostAction(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Post Created", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get post
|
|
||||||
export const getPost = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get(`/posts/${id}`);
|
|
||||||
|
|
||||||
dispatch(getPostAction(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add comment
|
|
||||||
export const addComment = (postId, formData) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.post(`/posts/comment/${postId}`, formData);
|
|
||||||
|
|
||||||
dispatch(addCommentAction(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Comment Added", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete comment
|
|
||||||
export const deleteComment = (postId, commentId) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
await api.delete(`/posts/comment/${postId}/${commentId}`);
|
|
||||||
|
|
||||||
dispatch(removeComment(commentId));
|
|
||||||
|
|
||||||
dispatch(createAlert("Comment Removed", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
postError({ msg: err.response.statusText, status: err.response.status })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import api from "../utils/api";
|
|
||||||
import { createAlert } from "./alert";
|
|
||||||
import {
|
|
||||||
noRepos,
|
|
||||||
getRepos,
|
|
||||||
clearProfile,
|
|
||||||
profileError,
|
|
||||||
getProfilesType,
|
|
||||||
updateProfile,
|
|
||||||
getProfile,
|
|
||||||
} from "../reducers/profile";
|
|
||||||
import { accountDeleted } from "../reducers/auth";
|
|
||||||
|
|
||||||
|
|
||||||
// Get current users profile
|
|
||||||
export const getCurrentProfile = () => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get("/profile/me");
|
|
||||||
|
|
||||||
dispatch(getProfile(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all profiles
|
|
||||||
export const getProfiles = () => async (dispatch) => {
|
|
||||||
dispatch(clearProfile());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.get("/profile");
|
|
||||||
|
|
||||||
dispatch(getProfilesType(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get profile by ID
|
|
||||||
export const getProfileById = (userId) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get(`/profile/user/${userId}`);
|
|
||||||
|
|
||||||
dispatch(getProfile(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get Github repos
|
|
||||||
export const getGithubRepos = (username) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get(`/profile/github/${username}`);
|
|
||||||
|
|
||||||
dispatch(getRepos(res.data));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(noRepos());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create or update profile
|
|
||||||
export const createProfile =
|
|
||||||
(formData, edit = false) =>
|
|
||||||
async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.post("/profile", formData);
|
|
||||||
|
|
||||||
dispatch(getProfile(res.data));
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
createAlert(edit ? "Profile Updated" : "Profile Created", "success")
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const errors = err.response.data.errors;
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add Experience
|
|
||||||
export const addExperience = (formData) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.put("/profile/experience", formData);
|
|
||||||
|
|
||||||
dispatch(updateProfile(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Experience Added", "success"));
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const errors = err.response.data.errors;
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add Education
|
|
||||||
export const addEducation = (formData) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.put("/profile/education", formData);
|
|
||||||
|
|
||||||
dispatch(updateProfile(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Education Added", "success"));
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const errors = err.response.data.errors;
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete experience
|
|
||||||
export const deleteExperience = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.delete(`/profile/experience/${id}`);
|
|
||||||
|
|
||||||
dispatch(updateProfile(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Experience Removed", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete education
|
|
||||||
export const deleteEducation = (id) => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const res = await api.delete(`/profile/education/${id}`);
|
|
||||||
|
|
||||||
dispatch(updateProfile(res.data));
|
|
||||||
|
|
||||||
dispatch(createAlert("Education Removed", "success"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete account & profile
|
|
||||||
export const deleteAccount = () => async (dispatch) => {
|
|
||||||
if (window.confirm("Are you sure? This can NOT be undone!")) {
|
|
||||||
try {
|
|
||||||
await api.delete("/profile");
|
|
||||||
|
|
||||||
dispatch(clearProfile());
|
|
||||||
dispatch(accountDeleted());
|
|
||||||
|
|
||||||
dispatch(createAlert("Your account has been permanently deleted"));
|
|
||||||
} catch (err) {
|
|
||||||
dispatch(
|
|
||||||
profileError({
|
|
||||||
msg: err.response.statusText,
|
|
||||||
status: err.response.status,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Link, Navigate } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { login } from "../../actions/auth";
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { email, password } = formData;
|
|
||||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
|
|
||||||
const onChange = (e) =>
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
|
|
||||||
const onSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(login(email, password));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <Navigate to="/dashboard" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Sign In</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-user" /> Sign Into Your Account
|
|
||||||
</p>
|
|
||||||
<form className="form" onSubmit={onSubmit}>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email Address"
|
|
||||||
name="email"
|
|
||||||
value={email}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
name="password"
|
|
||||||
value={password}
|
|
||||||
onChange={onChange}
|
|
||||||
minLength="6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="submit" className="btn btn-primary" value="Login" />
|
|
||||||
</form>
|
|
||||||
<p className="my-1">
|
|
||||||
Don't have an account? <Link to="/register">Sign Up</Link>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useDispatch,useSelector } from 'react-redux';
|
|
||||||
import { Link, Navigate } from 'react-router-dom';
|
|
||||||
import { createAlert } from '../../actions/alert';
|
|
||||||
import { register } from '../../actions/auth';
|
|
||||||
|
|
||||||
|
|
||||||
const Register = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
password2: ''
|
|
||||||
});
|
|
||||||
const isAuthenticated = useSelector(state=>state.auth.isAuthenticated)
|
|
||||||
const { name, email, password, password2 } = formData;
|
|
||||||
|
|
||||||
const onChange = (e) =>
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
|
|
||||||
const onSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (password !== password2) {
|
|
||||||
await dispatch(createAlert('Passwords do not match', 'danger'));
|
|
||||||
} else {
|
|
||||||
await dispatch(register({ name, email, password }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <Navigate to="/dashboard" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Sign Up</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-user" /> Create Your Account
|
|
||||||
</p>
|
|
||||||
<form className="form" onSubmit={onSubmit}>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email Address"
|
|
||||||
name="email"
|
|
||||||
value={email}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
This site uses Gravatar so if you want a profile image, use a
|
|
||||||
Gravatar email
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
name="password"
|
|
||||||
value={password}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
name="password2"
|
|
||||||
value={password2}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="submit" className="btn btn-primary" value="Register" />
|
|
||||||
</form>
|
|
||||||
<p className="my-1">
|
|
||||||
Already have an account? <Link to="/login">Sign In</Link>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Register;
|
|
||||||
@@ -59,7 +59,6 @@ export default function SignUp() {
|
|||||||
name="name"
|
name="name"
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
id="name"
|
|
||||||
label="Name"
|
label="Name"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@@ -68,7 +67,6 @@ export default function SignUp() {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
id="email"
|
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
name="email"
|
name="email"
|
||||||
/>
|
/>
|
||||||
@@ -80,7 +78,6 @@ export default function SignUp() {
|
|||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
@@ -90,7 +87,6 @@ export default function SignUp() {
|
|||||||
name="password2"
|
name="password2"
|
||||||
label="Repeat Password"
|
label="Repeat Password"
|
||||||
type="password"
|
type="password"
|
||||||
id="password2"
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import DashboardActions from "./DashboardActions";
|
|
||||||
import Experience from "./Experience";
|
|
||||||
import Education from "./Education";
|
|
||||||
import { getCurrentProfile, deleteAccount } from "../../actions/profile";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
|
||||||
function fetchData() {
|
|
||||||
dispatch(getCurrentProfile());
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch]);
|
|
||||||
const user = useSelector((state) => state.auth.user);
|
|
||||||
const profile = useSelector((state) => state.profile.profile);
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Dashboard</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-user" /> Welcome {user && user.name}
|
|
||||||
</p>
|
|
||||||
{profile !== null ? (
|
|
||||||
<>
|
|
||||||
<DashboardActions />
|
|
||||||
<Experience experience={profile.experience} />
|
|
||||||
<Education education={profile.education} />
|
|
||||||
|
|
||||||
<div className="my-2">
|
|
||||||
<button
|
|
||||||
className="btn btn-danger"
|
|
||||||
onClick={async () => await dispatch(deleteAccount())}
|
|
||||||
>
|
|
||||||
<i className="fas fa-user" /> Delete My Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>You have not yet setup a profile, please add some info</p>
|
|
||||||
<Link to="/create-profile" className="btn btn-primary my-1">
|
|
||||||
Create Profile
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
const DashboardActions = () => {
|
|
||||||
return (
|
|
||||||
<div className='dash-buttons'>
|
|
||||||
<Link to='/edit-profile' className='btn btn-light'>
|
|
||||||
<i className='fas fa-user-circle text-primary' /> Edit Profile
|
|
||||||
</Link>
|
|
||||||
<Link to='/add-experience' className='btn btn-light'>
|
|
||||||
<i className='fab fa-black-tie text-primary' /> Add Experience
|
|
||||||
</Link>
|
|
||||||
<Link to='/add-education' className='btn btn-light'>
|
|
||||||
<i className='fas fa-graduation-cap text-primary' /> Add Education
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardActions;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import React, { Fragment } from "react";
|
|
||||||
import {useDispatch } from "react-redux";
|
|
||||||
import { deleteEducation } from "../../actions/profile";
|
|
||||||
import formatDate from "../../utils/formatDate";
|
|
||||||
|
|
||||||
const Education = ({education}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const educations = education.map((edu) => (
|
|
||||||
<tr key={edu._id}>
|
|
||||||
<td>{edu.school}</td>
|
|
||||||
<td className="hide-sm">{edu.degree}</td>
|
|
||||||
<td>
|
|
||||||
{formatDate(edu.from)} - {edu.to ? formatDate(edu.to) : "Now"}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
onClick={async () => await dispatch(deleteEducation(edu._id))}
|
|
||||||
className="btn btn-danger"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<h2 className="my-2">Education Credentials</h2>
|
|
||||||
<table className="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>School</th>
|
|
||||||
<th className="hide-sm">Degree</th>
|
|
||||||
<th className="hide-sm">Years</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>{educations}</tbody>
|
|
||||||
</table>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Education;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import {useDispatch } from 'react-redux';
|
|
||||||
import { deleteExperience } from '../../actions/profile';
|
|
||||||
import formatDate from '../../utils/formatDate';
|
|
||||||
|
|
||||||
const Experience = ({experience}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const experiences = experience.map((exp) => (
|
|
||||||
<tr key={exp._id}>
|
|
||||||
<td>{exp.company}</td>
|
|
||||||
<td className="hide-sm">{exp.title}</td>
|
|
||||||
<td>
|
|
||||||
{formatDate(exp.from)} - {exp.to ? formatDate(exp.to) : 'Now'}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
onClick={async () => dispatch(deleteExperience(exp._id))}
|
|
||||||
className="btn btn-danger"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<h2 className="my-2">Experience Credentials</h2>
|
|
||||||
<table className="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Company</th>
|
|
||||||
<th className="hide-sm">Title</th>
|
|
||||||
<th className="hide-sm">Years</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>{experiences}</tbody>
|
|
||||||
</table>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default Experience;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
const Alert = () => {
|
|
||||||
const alerts = useSelector((state) => state.alert);
|
|
||||||
return (
|
|
||||||
<div className="alert-wrapper">
|
|
||||||
{alerts.map((alert) => (
|
|
||||||
<div key={alert.id} className={`alert alert-${alert.alertType}`}>
|
|
||||||
{alert.msg}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Alert;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link, Navigate } from "react-router-dom";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
|
|
||||||
const Landing = () => {
|
|
||||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <Navigate to="/dashboard" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="landing">
|
|
||||||
<div className="dark-overlay">
|
|
||||||
<div className="landing-inner">
|
|
||||||
<h1 className="x-large">Developer Connector</h1>
|
|
||||||
<p className="lead">
|
|
||||||
Create a developer profile/portfolio, share posts and get help from
|
|
||||||
other developers
|
|
||||||
</p>
|
|
||||||
<div className="buttons">
|
|
||||||
<Link to="/register" className="btn btn-primary">
|
|
||||||
Sign Up
|
|
||||||
</Link>
|
|
||||||
<Link to="/login" className="btn btn-light">
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Landing;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React, { Fragment } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { logOut } from "../../reducers/auth";
|
|
||||||
|
|
||||||
const Navbar = () => {
|
|
||||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const authLinks = (
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<Link to="/profiles">Developers</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/posts">Posts</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/dashboard">
|
|
||||||
<i className="fas fa-user" /> <span className="hide-sm">Profile</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a onClick={()=>dispatch(logOut())} href="#!">
|
|
||||||
<i className="fas fa-sign-out-alt" />{' '}
|
|
||||||
<span className="hide-sm">Logout</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
|
|
||||||
const guestLinks = (
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<Link to="/profiles">Developers</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/register">Register</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/login">Login</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="navbar bg-dark">
|
|
||||||
<h1>
|
|
||||||
<Link to="/posts">
|
|
||||||
<i className="fas fa-code" /> DevConnector
|
|
||||||
</Link>
|
|
||||||
</h1>
|
|
||||||
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Navbar;
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logOut } from "../../reducers/auth";
|
import { logOut } from "../../reducers/auth";
|
||||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||||
@@ -69,7 +68,7 @@ const Navbar = () => {
|
|||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth="xl">
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
{/*Icon for big dislay*/}
|
{/*Icon for big display*/}
|
||||||
<AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
|
<AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
|
||||||
{/*LOGO link for bi display*/}
|
{/*LOGO link for bi display*/}
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const NotFound = () => {
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="x-large text-primary">
|
|
||||||
<i className="fas fa-exclamation-triangle" /> Page Not Found
|
|
||||||
</h1>
|
|
||||||
<p className="large">Sorry, this page does not exist</p>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotFound;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import spinner from './spinner.gif';
|
|
||||||
|
|
||||||
const Spinner = () => (
|
|
||||||
<Fragment>
|
|
||||||
<img
|
|
||||||
src={spinner}
|
|
||||||
style={{ width: '200px', margin: 'auto', display: 'block' }}
|
|
||||||
alt="Loading..."
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Spinner;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { addComment } from '../../actions/post';
|
|
||||||
|
|
||||||
const CommentForm = ({ postId }) => {
|
|
||||||
const [text, setText] = useState('');
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='post-form'>
|
|
||||||
<div className='bg-primary p'>
|
|
||||||
<h3>Leave a Comment</h3>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
className='form my-1'
|
|
||||||
onSubmit={async(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(addComment(postId, { text }));
|
|
||||||
setText('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
name='text'
|
|
||||||
cols='30'
|
|
||||||
rows='5'
|
|
||||||
placeholder='Comment the post'
|
|
||||||
value={text}
|
|
||||||
onChange={e => setText(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input type='submit' className='btn btn-dark my-1' value='Submit' />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default CommentForm
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import formatDate from "../../utils/formatDate";
|
|
||||||
import { deleteComment } from "../../actions/post";
|
|
||||||
|
|
||||||
const CommentItem = ({
|
|
||||||
postId,
|
|
||||||
comment: { _id, text, name, avatar, user, date },
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const auth = useSelector((state) => state.auth);
|
|
||||||
return (
|
|
||||||
<div className="post bg-white p-1 my-1">
|
|
||||||
<div>
|
|
||||||
<Link to={`/profile/${user}`}>
|
|
||||||
<img className="round-img" src={avatar} alt="" />
|
|
||||||
<h4>{name}</h4>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="my-1">{text}</p>
|
|
||||||
<p className="post-date">Posted on {formatDate(date)}</p>
|
|
||||||
{!auth.loading && user === auth.user._id && (
|
|
||||||
<button
|
|
||||||
onClick={async () => await dispatch(deleteComment(postId, _id))}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-danger"
|
|
||||||
>
|
|
||||||
<i className="fas fa-times" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommentItem;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { Link, useParams } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import Spinner from "../layout/Spinner";
|
|
||||||
import PostItem from "../posts/PostItem";
|
|
||||||
import CommentForm from "../post/CommentForm";
|
|
||||||
import CommentItem from "../post/CommentItem";
|
|
||||||
import { getPost } from "../../actions/post";
|
|
||||||
|
|
||||||
const Post = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { post, loading } = useSelector((state) => state.post);
|
|
||||||
const { id } = useParams();
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
await dispatch(getPost(id));
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch, id]);
|
|
||||||
|
|
||||||
return loading || post === null ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<section className="container">
|
|
||||||
<Link to="/posts" className="btn">
|
|
||||||
Back To Posts
|
|
||||||
</Link>
|
|
||||||
<PostItem post={post} showActions={false} />
|
|
||||||
<CommentForm postId={post._id} />
|
|
||||||
<div className="comments">
|
|
||||||
{post.comments.map((comment) => (
|
|
||||||
<CommentItem key={comment._id} comment={comment} postId={post._id} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Post;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { addPost } from "../../actions/post";
|
|
||||||
|
|
||||||
const PostForm = () => {
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
return (
|
|
||||||
<div className="post-form">
|
|
||||||
<div className="bg-primary p">
|
|
||||||
<h3>Say Something...</h3>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
className="form my-1"
|
|
||||||
onSubmit={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(addPost({ text }));
|
|
||||||
setText("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
name="text"
|
|
||||||
cols="30"
|
|
||||||
rows="5"
|
|
||||||
placeholder="Create a post"
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => setText(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input type="submit" className="btn btn-dark my-1" value="Submit" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PostForm;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import formatDate from "../../utils/formatDate";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { addLike, removeLike, deletePost } from "../../actions/post";
|
|
||||||
|
|
||||||
const PostItem = ({
|
|
||||||
post: { _id, text, name, avatar, user, likes, comments, date },
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const auth = useSelector((state) => state.auth);
|
|
||||||
return (
|
|
||||||
<div className="post bg-white p-1 my-1">
|
|
||||||
<div>
|
|
||||||
<Link to={`/profile/${user}`}>
|
|
||||||
<img className="round-img" src={avatar} alt="" />
|
|
||||||
<h4>{name}</h4>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="my-1">{text}</p>
|
|
||||||
<p className="post-date">Posted on {formatDate(date)}</p>
|
|
||||||
<button
|
|
||||||
onClick={async () => await dispatch(addLike(_id))}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-light"
|
|
||||||
>
|
|
||||||
<i className="fas fa-thumbs-up" />{" "}
|
|
||||||
<span>{likes.length > 0 && <span>{likes.length}</span>}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={async () => await dispatch(removeLike(_id))}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-light"
|
|
||||||
>
|
|
||||||
<i className="fas fa-thumbs-down" />
|
|
||||||
</button>
|
|
||||||
<Link to={`/posts/${_id}`} className="btn btn-primary">
|
|
||||||
Discussion{" "}
|
|
||||||
{comments.length > 0 && (
|
|
||||||
<span className="comment-count">{comments.length}</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
{!auth.loading && user === auth.user._id && (
|
|
||||||
<button
|
|
||||||
onClick={async () => await dispatch(deletePost(_id))}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-danger"
|
|
||||||
>
|
|
||||||
<i className="fas fa-times" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default PostItem;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import PostItem from "./PostItem";
|
|
||||||
import PostForm from "./PostForm";
|
|
||||||
import { getPosts } from "../../actions/post";
|
|
||||||
|
|
||||||
const Posts = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
await dispatch(getPosts());
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch]);
|
|
||||||
const posts = useSelector((state) => state.post.posts);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Posts</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-user" /> Welcome to the community
|
|
||||||
</p>
|
|
||||||
<PostForm />
|
|
||||||
<div className="posts">
|
|
||||||
{posts.map((post) => (
|
|
||||||
<PostItem key={post._id} post={post} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Posts;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { addEducation } from "../../actions/profile";
|
|
||||||
|
|
||||||
const AddEducation = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
school: "",
|
|
||||||
degree: "",
|
|
||||||
fieldofstudy: "",
|
|
||||||
from: "",
|
|
||||||
to: "",
|
|
||||||
current: false,
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { school, degree, fieldofstudy, from, to, description, current } =
|
|
||||||
formData;
|
|
||||||
|
|
||||||
const onChange = (e) =>
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Add Your Education</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-code-branch" /> Add any school or bootcamp that you
|
|
||||||
have attended
|
|
||||||
</p>
|
|
||||||
<small>* = required field</small>
|
|
||||||
<form
|
|
||||||
className="form"
|
|
||||||
onSubmit={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(addEducation(formData)).then(() => navigate("/dashboard"));
|
|
||||||
// i have no idea how this works used to work, i removed the navigate function from the addEducation and it does now
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="* School or Bootcamp"
|
|
||||||
name="school"
|
|
||||||
value={school}
|
|
||||||
onChange={onChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="* Degree or Certificate"
|
|
||||||
name="degree"
|
|
||||||
value={degree}
|
|
||||||
onChange={onChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Field of Study"
|
|
||||||
name="fieldofstudy"
|
|
||||||
value={fieldofstudy}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<h4>From Date</h4>
|
|
||||||
<input type="date" name="from" value={from} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<p>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="current"
|
|
||||||
checked={current}
|
|
||||||
value={current}
|
|
||||||
onChange={() => setFormData({ ...formData, current: !current })}
|
|
||||||
/>{" "}
|
|
||||||
Current School
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<h4>To Date</h4>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="to"
|
|
||||||
value={to}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={current}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
cols="30"
|
|
||||||
rows="5"
|
|
||||||
placeholder="Program Description"
|
|
||||||
value={description}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="submit" className="btn btn-primary my-1" />
|
|
||||||
<Link className="btn btn-light my-1" to="/dashboard">
|
|
||||||
Go Back
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default AddEducation;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { addExperience } from '../../actions/profile';
|
|
||||||
|
|
||||||
const AddExperience = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
company: '',
|
|
||||||
title: '',
|
|
||||||
location: '',
|
|
||||||
from: '',
|
|
||||||
to: '',
|
|
||||||
current: false,
|
|
||||||
description: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const { company, title, location, from, to, current, description } = formData;
|
|
||||||
|
|
||||||
const onChange = (e) =>
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">Add An Experience</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-code-branch" /> Add any developer/programming
|
|
||||||
positions that you have had in the past
|
|
||||||
</p>
|
|
||||||
<small>* = required field</small>
|
|
||||||
<form
|
|
||||||
className="form"
|
|
||||||
onSubmit={async(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(addExperience(formData)).then(() => navigate('/dashboard'));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="* Job Title"
|
|
||||||
name="title"
|
|
||||||
value={title}
|
|
||||||
onChange={onChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="* Company"
|
|
||||||
name="company"
|
|
||||||
value={company}
|
|
||||||
onChange={onChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Location"
|
|
||||||
name="location"
|
|
||||||
value={location}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<h4>From Date</h4>
|
|
||||||
<input type="date" name="from" value={from} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<p>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="current"
|
|
||||||
checked={current}
|
|
||||||
value={current}
|
|
||||||
onChange={() => {
|
|
||||||
setFormData({ ...formData, current: !current });
|
|
||||||
}}
|
|
||||||
/>{' '}
|
|
||||||
Current Job
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<h4>To Date</h4>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="to"
|
|
||||||
value={to}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={current}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
cols="30"
|
|
||||||
rows="5"
|
|
||||||
placeholder="Job Description"
|
|
||||||
value={description}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input type="submit" className="btn btn-primary my-1" />
|
|
||||||
<Link className="btn btn-light my-1" to="/dashboard">
|
|
||||||
Go Back
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddExperience;
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
import React, { Fragment, useState, useEffect } from "react";
|
|
||||||
import { Link, useMatch, useNavigate } from "react-router-dom";
|
|
||||||
import { createProfile, getCurrentProfile } from "../../actions/profile";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
company: "",
|
|
||||||
website: "",
|
|
||||||
location: "",
|
|
||||||
status: "",
|
|
||||||
skills: "",
|
|
||||||
githubusername: "",
|
|
||||||
bio: "",
|
|
||||||
twitter: "",
|
|
||||||
facebook: "",
|
|
||||||
linkedin: "",
|
|
||||||
youtube: "",
|
|
||||||
instagram: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProfileForm = () => {
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { profile, loading } = useSelector((state) => state.profile);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState(initialState);
|
|
||||||
|
|
||||||
const creatingProfile = useMatch("/create-profile");
|
|
||||||
|
|
||||||
const [displaySocialInputs, toggleSocialInputs] = useState(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// if there is no profile, attempt to fetch one
|
|
||||||
async function fetchData(){
|
|
||||||
await dispatch(getCurrentProfile())
|
|
||||||
}
|
|
||||||
if (!profile) fetchData();
|
|
||||||
|
|
||||||
// if we finished loading and we do have a profile
|
|
||||||
// then build our profileData
|
|
||||||
if (!loading && profile) {
|
|
||||||
const profileData = { ...initialState };
|
|
||||||
for (const key in profile) {
|
|
||||||
if (key in profileData) profileData[key] = profile[key];
|
|
||||||
}
|
|
||||||
for (const key in profile.social) {
|
|
||||||
if (key in profileData) profileData[key] = profile.social[key];
|
|
||||||
}
|
|
||||||
// the skills may be an array from our API response
|
|
||||||
if (Array.isArray(profileData.skills))
|
|
||||||
profileData.skills = profileData.skills.join(", ");
|
|
||||||
// set local state with the profileData
|
|
||||||
setFormData(profileData);
|
|
||||||
}
|
|
||||||
}, [loading, dispatch, profile]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
company,
|
|
||||||
website,
|
|
||||||
location,
|
|
||||||
status,
|
|
||||||
skills,
|
|
||||||
githubusername,
|
|
||||||
bio,
|
|
||||||
twitter,
|
|
||||||
facebook,
|
|
||||||
linkedin,
|
|
||||||
youtube,
|
|
||||||
instagram,
|
|
||||||
} = formData;
|
|
||||||
|
|
||||||
const onChange = (e) =>
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
|
|
||||||
const onSubmit = async (e) => {
|
|
||||||
const editing = profile ? true : false;
|
|
||||||
e.preventDefault();
|
|
||||||
await dispatch(createProfile(formData, editing)).then(() => {
|
|
||||||
console.log(editing)
|
|
||||||
if (!editing) navigate("/dashboard");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
<h1 className="large text-primary">
|
|
||||||
{creatingProfile ? "Create Your Profile" : "Edit Your Profile"}
|
|
||||||
</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fas fa-user" />
|
|
||||||
{creatingProfile
|
|
||||||
? ` Let's get some information to make your`
|
|
||||||
: " Add some changes to your profile"}
|
|
||||||
</p>
|
|
||||||
<small>* = required field</small>
|
|
||||||
<form className="form" onSubmit={onSubmit}>
|
|
||||||
<div className="form-group">
|
|
||||||
<select name="status" value={status} onChange={onChange}>
|
|
||||||
<option>* Select Professional Status</option>
|
|
||||||
<option value="Developer">Developer</option>
|
|
||||||
<option value="Junior Developer">Junior Developer</option>
|
|
||||||
<option value="Senior Developer">Senior Developer</option>
|
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
<option value="Student or Learning">Student or Learning</option>
|
|
||||||
<option value="Instructor">Instructor or Teacher</option>
|
|
||||||
<option value="Intern">Intern</option>
|
|
||||||
<option value="Other">Other</option>
|
|
||||||
</select>
|
|
||||||
<small className="form-text">
|
|
||||||
Give us an idea of where you are at in your career
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Company"
|
|
||||||
name="company"
|
|
||||||
value={company}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
Could be your own company or one you work for
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Website"
|
|
||||||
name="website"
|
|
||||||
value={website}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
Could be your own or a company website
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Location"
|
|
||||||
name="location"
|
|
||||||
value={location}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
City & state suggested (eg. Boston, MA)
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="* Skills"
|
|
||||||
name="skills"
|
|
||||||
value={skills}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Github Username"
|
|
||||||
name="githubusername"
|
|
||||||
value={githubusername}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">
|
|
||||||
If you want your latest repos and a Github link, include your
|
|
||||||
username
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<textarea
|
|
||||||
placeholder="A short bio of yourself"
|
|
||||||
name="bio"
|
|
||||||
value={bio}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<small className="form-text">Tell us a little about yourself</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-2">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSocialInputs(!displaySocialInputs)}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-light"
|
|
||||||
>
|
|
||||||
Add Social Network Links
|
|
||||||
</button>
|
|
||||||
<span>Optional</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{displaySocialInputs && (
|
|
||||||
<Fragment>
|
|
||||||
<div className="form-group social-input">
|
|
||||||
<i className="fab fa-twitter fa-2x" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Twitter URL"
|
|
||||||
name="twitter"
|
|
||||||
value={twitter}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group social-input">
|
|
||||||
<i className="fab fa-facebook fa-2x" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Facebook URL"
|
|
||||||
name="facebook"
|
|
||||||
value={facebook}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group social-input">
|
|
||||||
<i className="fab fa-youtube fa-2x" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="YouTube URL"
|
|
||||||
name="youtube"
|
|
||||||
value={youtube}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group social-input">
|
|
||||||
<i className="fab fa-linkedin fa-2x" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Linkedin URL"
|
|
||||||
name="linkedin"
|
|
||||||
value={linkedin}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group social-input">
|
|
||||||
<i className="fab fa-instagram fa-2x" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Instagram URL"
|
|
||||||
name="instagram"
|
|
||||||
value={instagram}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input type="submit" className="btn btn-primary my-1" />
|
|
||||||
<Link className="btn btn-light my-1" to="/dashboard">
|
|
||||||
Go Back
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileForm;
|
|
||||||
@@ -19,10 +19,10 @@ const initialState = {
|
|||||||
company: "",
|
company: "",
|
||||||
website: "",
|
website: "",
|
||||||
location: "",
|
location: "",
|
||||||
status: "",
|
|
||||||
skills: "",
|
skills: "",
|
||||||
githubusername: "",
|
githubusername: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
|
status: "",
|
||||||
twitter: "",
|
twitter: "",
|
||||||
facebook: "",
|
facebook: "",
|
||||||
linkedin: "",
|
linkedin: "",
|
||||||
@@ -55,7 +55,7 @@ const ProfileForm = () => {
|
|||||||
// then build our profileData
|
// then build our profileData
|
||||||
if (!loading && profile) {
|
if (!loading && profile) {
|
||||||
const profileData: any = { ...initialState };
|
const profileData: any = { ...initialState };
|
||||||
// cant figure out how to type key to be keyof profile so they are any now
|
// can't figure out how to type key to be keyof profile so they are any now
|
||||||
for (const key in profile) {
|
for (const key in profile) {
|
||||||
if (key in profileData) profileData[key] = profile[key];
|
if (key in profileData) profileData[key] = profile[key];
|
||||||
}
|
}
|
||||||
@@ -82,16 +82,19 @@ const ProfileForm = () => {
|
|||||||
youtube,
|
youtube,
|
||||||
linkedin,
|
linkedin,
|
||||||
instagram,
|
instagram,
|
||||||
status,
|
|
||||||
} = formData;
|
} = formData;
|
||||||
|
|
||||||
const onChange = (
|
const onChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => setFormData({ ...formData, [e.target.name]: e.target.value });
|
) => {
|
||||||
|
return setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
const editing = profile ? true : false;
|
const editing = profile ? true : false;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const form = new FormData(e.currentTarget);
|
||||||
|
const status = form.get("status") as string;
|
||||||
if (facebook?.length > 100) {
|
if (facebook?.length > 100) {
|
||||||
dispatch(createAlert("Facebook link is longer 100 characters", "danger"));
|
dispatch(createAlert("Facebook link is longer 100 characters", "danger"));
|
||||||
} else if (linkedin?.length > 100) {
|
} else if (linkedin?.length > 100) {
|
||||||
@@ -108,9 +111,9 @@ const ProfileForm = () => {
|
|||||||
dispatch(createAlert("Skills is longer 100 characters", "danger"));
|
dispatch(createAlert("Skills is longer 100 characters", "danger"));
|
||||||
} else if (skills.length === 0) {
|
} else if (skills.length === 0) {
|
||||||
dispatch(createAlert("Skills is required", "danger"));
|
dispatch(createAlert("Skills is required", "danger"));
|
||||||
} else if (skills.length > 100 || skills.length === 0) {
|
|
||||||
dispatch(createAlert("Status is required", "danger"));
|
|
||||||
} else if (status?.length === 0) {
|
} else if (status?.length === 0) {
|
||||||
|
dispatch(createAlert("Status is required", "danger"));
|
||||||
|
} else if (location?.length > 100) {
|
||||||
dispatch(createAlert("Location is longer 100 characters", "danger"));
|
dispatch(createAlert("Location is longer 100 characters", "danger"));
|
||||||
} else if (githubusername?.length > 50) {
|
} else if (githubusername?.length > 50) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -212,12 +215,12 @@ const ProfileForm = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<Typography>
|
<Typography>
|
||||||
Please use comma separeted values (eg. HTML, CSS, JavaScript, PHP)
|
Please use comma separated values (eg. HTML, CSS, JavaScript, PHP)
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
name="githubUser"
|
name="githubusername"
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Github Username"
|
label="Github Username"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -237,9 +240,8 @@ const ProfileForm = () => {
|
|||||||
labelId="status"
|
labelId="status"
|
||||||
name="status"
|
name="status"
|
||||||
required
|
required
|
||||||
placeholder="Select Profesional status"
|
placeholder="Select Professional status"
|
||||||
defaultValue={""}
|
defaultValue={""}
|
||||||
value={status}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="">None</MenuItem>
|
<MenuItem value="">None</MenuItem>
|
||||||
<MenuItem value="Developer">Developer</MenuItem>
|
<MenuItem value="Developer">Developer</MenuItem>
|
||||||
@@ -256,7 +258,7 @@ const ProfileForm = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<Typography paddingTop="2.3rem">
|
<Typography paddingTop="2.3rem">
|
||||||
Select Profesional Status
|
Select Professional Status
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React, { Fragment, useEffect } from "react";
|
|
||||||
import { Link, useParams } from "react-router-dom";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import Spinner from "../layout/Spinner";
|
|
||||||
import ProfileTop from "./ProfileTop";
|
|
||||||
import ProfileAbout from "./ProfileAbout";
|
|
||||||
import ProfileExperience from "./ProfileExperience";
|
|
||||||
import ProfileEducation from "./ProfileEducation";
|
|
||||||
import ProfileGithub from "./ProfileGithub";
|
|
||||||
import { getProfileById } from "../../actions/profile";
|
|
||||||
|
|
||||||
const Profile = () => {
|
|
||||||
const profile = useSelector((state) => state.profile.profile);
|
|
||||||
const auth = useSelector((state) => state.auth);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { id } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData(){
|
|
||||||
await dispatch(getProfileById(id));
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch,id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
{profile === null ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
<Link to="/profiles" className="btn btn-light">
|
|
||||||
Back To Profiles
|
|
||||||
</Link>
|
|
||||||
{auth.isAuthenticated &&
|
|
||||||
auth.loading === false &&
|
|
||||||
auth.user._id === profile.user._id && (
|
|
||||||
<Link to="/edit-profile" className="btn btn-dark">
|
|
||||||
Edit Profile
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<div className="profile-grid my-1">
|
|
||||||
<ProfileTop profile={profile} />
|
|
||||||
<ProfileAbout profile={profile} />
|
|
||||||
<div className="profile-exp bg-white p-2">
|
|
||||||
<h2 className="text-primary">Experience</h2>
|
|
||||||
{profile.experience.length > 0 ? (
|
|
||||||
<Fragment>
|
|
||||||
{profile.experience.map((experience) => (
|
|
||||||
<ProfileExperience
|
|
||||||
key={experience._id}
|
|
||||||
experience={experience}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
) : (
|
|
||||||
<h4>No experience credentials</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-edu bg-white p-2">
|
|
||||||
<h2 className="text-primary">Education</h2>
|
|
||||||
{profile.education.length > 0 ? (
|
|
||||||
<Fragment>
|
|
||||||
{profile.education.map((education) => (
|
|
||||||
<ProfileEducation
|
|
||||||
key={education._id}
|
|
||||||
education={education}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
) : (
|
|
||||||
<h4>No education credentials</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{profile.githubusername && (
|
|
||||||
<ProfileGithub username={profile.githubusername} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Profile;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
|
|
||||||
const ProfileAbout = ({
|
|
||||||
profile: {
|
|
||||||
bio,
|
|
||||||
skills,
|
|
||||||
user: { name }
|
|
||||||
}
|
|
||||||
}) => (
|
|
||||||
<div className='profile-about bg-light p-2'>
|
|
||||||
{bio && (
|
|
||||||
<Fragment>
|
|
||||||
<h2 className='text-primary'>{name.trim().split(' ')[0]}s Bio</h2>
|
|
||||||
<p>{bio}</p>
|
|
||||||
<div className='line' />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<h2 className='text-primary'>Skill Set</h2>
|
|
||||||
<div className='skills'>
|
|
||||||
{skills.map((skill, index) => (
|
|
||||||
<div key={index} className='p-1'>
|
|
||||||
<i className='fas fa-check' /> {skill}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export default ProfileAbout;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import formatDate from '../../utils/formatDate';
|
|
||||||
|
|
||||||
const ProfileEducation = ({
|
|
||||||
education: { school, degree, fieldofstudy, current, to, from, description }
|
|
||||||
}) => (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-dark">{school}</h3>
|
|
||||||
<p>
|
|
||||||
{formatDate(from)} - {to ? formatDate(to) : 'Now'}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Degree: </strong> {degree}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Field Of Study: </strong> {fieldofstudy}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Description: </strong> {description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default ProfileEducation;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import formatDate from '../../utils/formatDate';
|
|
||||||
|
|
||||||
const ProfileExperience = ({
|
|
||||||
experience: { company, title, location, current, to, from, description }
|
|
||||||
}) => (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-dark">{company}</h3>
|
|
||||||
<p>
|
|
||||||
{formatDate(from)} - {to ? formatDate(to) : 'Now'}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Position: </strong> {title}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Location: </strong> {location}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Description: </strong> {description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export default ProfileExperience;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { getGithubRepos } from "../../actions/profile";
|
|
||||||
|
|
||||||
const ProfileGithub = ({ username }) => {
|
|
||||||
const repos = useSelector((state) => state.profile.repos);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
await dispatch(getGithubRepos(username));
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch, username]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="profile-github">
|
|
||||||
<h2 className="text-primary my-1">Github Repos</h2>
|
|
||||||
{repos.map((repo) => (
|
|
||||||
<div key={repo.id} className="repo bg-white p-1 my-1">
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
|
|
||||||
{repo.name}
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>{repo.description}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ul>
|
|
||||||
<li className="badge badge-primary">
|
|
||||||
Stars: {repo.stargazers_count}
|
|
||||||
</li>
|
|
||||||
<li className="badge badge-dark">
|
|
||||||
Watchers: {repo.watchers_count}
|
|
||||||
</li>
|
|
||||||
<li className="badge badge-light">Forks: {repo.forks_count}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileGithub;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const ProfileTop = ({
|
|
||||||
profile: {
|
|
||||||
status,
|
|
||||||
company,
|
|
||||||
location,
|
|
||||||
website,
|
|
||||||
social,
|
|
||||||
user: { name, avatar },
|
|
||||||
},
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="profile-top bg-primary p-2">
|
|
||||||
<img className="round-img my-1" src={avatar} alt="" />
|
|
||||||
<h1 className="large">{name}</h1>
|
|
||||||
<p className="lead">
|
|
||||||
{status} {company ? <span> at {company}</span> : null}
|
|
||||||
</p>
|
|
||||||
<p>{location ? <span>{location}</span> : null}</p>
|
|
||||||
<div className="icons my-1">
|
|
||||||
{website ? (
|
|
||||||
<a href={website} target="_blank" rel="noopener noreferrer">
|
|
||||||
<i className="fas fa-globe fa-2x" />
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
{social
|
|
||||||
? Object.entries(social)
|
|
||||||
.filter(([_, value]) => value)
|
|
||||||
.map(([key, value]) => (
|
|
||||||
<a
|
|
||||||
key={key}
|
|
||||||
href={value}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<i className={`fab fa-${key} fa-2x`}></i>
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileTop;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
const ProfileItem = ({
|
|
||||||
profile: {
|
|
||||||
user: { _id, name, avatar },
|
|
||||||
status,
|
|
||||||
company,
|
|
||||||
location,
|
|
||||||
skills
|
|
||||||
}
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className='profile bg-light'>
|
|
||||||
<img src={avatar} alt='' className='round-img' />
|
|
||||||
<div>
|
|
||||||
<h2>{name}</h2>
|
|
||||||
<p>
|
|
||||||
{status} {company && <span> at {company}</span>}
|
|
||||||
</p>
|
|
||||||
<p className='my-1'>{location && <span>{location}</span>}</p>
|
|
||||||
<Link to={`/profile/${_id}`} className='btn btn-primary'>
|
|
||||||
View Profile
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{skills.slice(0, 4).map((skill, index) => (
|
|
||||||
<li key={index} className='text-primary'>
|
|
||||||
<i className='fas fa-check' /> {skill}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileItem;
|
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import React, { Fragment, useEffect } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import Spinner from "../layout/Spinner";
|
|
||||||
import ProfileItem from "./ProfileItem";
|
|
||||||
import { getProfiles } from "../../actions/profile";
|
|
||||||
|
|
||||||
const Profiles = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
await dispatch(getProfiles());
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const { profiles, loading } = useSelector((state) => state.profile);
|
|
||||||
return (
|
|
||||||
<section className="container">
|
|
||||||
{loading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
<h1 className="large text-primary">Developers</h1>
|
|
||||||
<p className="lead">
|
|
||||||
<i className="fab fa-connectdevelop" /> Browse and connect with
|
|
||||||
developers
|
|
||||||
</p>
|
|
||||||
<div className="profiles">
|
|
||||||
{profiles.length > 0 ? (
|
|
||||||
profiles.map((profile) => (
|
|
||||||
<ProfileItem key={profile._id} profile={profile} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<h4>No profiles found...</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Profiles;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Navigate } from "react-router-dom";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import Spinner from "../layout/Spinner";
|
|
||||||
|
|
||||||
const PrivateRoute = ({ component: Component }) => {
|
|
||||||
const { isAuthenticated, loading } = useSelector((state) => state.auth);
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
if (isAuthenticated) return <Component />;
|
|
||||||
|
|
||||||
return <Navigate to="/login" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrivateRoute;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {createRoot} from 'react-dom/client'
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
// Level - 0
|
|
||||||
//Program stars from Here. Imports and renders App.
|
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('root'));
|
|
||||||
root.render(<App/>)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
const initialState = [];
|
|
||||||
|
|
||||||
const alertSlice = createSlice({
|
|
||||||
name: "alert",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setAlert(state, action) {
|
|
||||||
return [...state, action.payload];
|
|
||||||
},
|
|
||||||
removeAlert(state, action) {
|
|
||||||
return state.filter((alert) => alert.id !== action.payload);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setAlert, removeAlert } = alertSlice.actions;
|
|
||||||
|
|
||||||
export default alertSlice.reducer;
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
token: localStorage.getItem("token"),
|
|
||||||
isAuthenticated: null,
|
|
||||||
loading: true,
|
|
||||||
user: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const authSlice = createSlice({
|
|
||||||
name: "auth",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
userLoaded(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
loading: false,
|
|
||||||
user: action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
registerSuccess(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.payload,
|
|
||||||
isAuthenticated: true,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
loginSucces(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.payload,
|
|
||||||
isAuthenticated: true,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
accountDeleted(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
loading: false,
|
|
||||||
user: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
authError(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
loading: false,
|
|
||||||
user: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
logOut(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
loading: false,
|
|
||||||
user: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const {
|
|
||||||
logOut,
|
|
||||||
userLoaded,
|
|
||||||
loginSucces,
|
|
||||||
registerSuccess,
|
|
||||||
accountDeleted,
|
|
||||||
authError,
|
|
||||||
} = authSlice.actions;
|
|
||||||
|
|
||||||
export default authSlice.reducer;
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
posts: [],
|
|
||||||
post: null,
|
|
||||||
loading: true,
|
|
||||||
error: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const postSlice = createSlice({
|
|
||||||
name: "post",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
getPostsAction(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
posts: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getPostAction(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
post: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
addPostAction(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
posts: [action.payload, ...state.posts],
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
deletePostAction(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
posts: state.posts.filter((post) => post._id !== action.payload),
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
postError(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updateLikes(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
posts: state.posts.map((post) =>
|
|
||||||
post._id === action.payload.id
|
|
||||||
? { ...post, likes: action.payload.likes }
|
|
||||||
: post
|
|
||||||
),
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
addCommentAction(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
post: { ...state.post, comments: action.payload },
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
removeComment(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
post: {
|
|
||||||
...state.post,
|
|
||||||
comments: state.post.comments.filter(
|
|
||||||
(comment) => comment._id !== action.payload
|
|
||||||
),
|
|
||||||
},
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const {
|
|
||||||
removeComment,
|
|
||||||
addCommentAction,
|
|
||||||
updateLikes,
|
|
||||||
postError,
|
|
||||||
deletePostAction,
|
|
||||||
getPostAction,
|
|
||||||
getPostsAction,
|
|
||||||
addPostAction,
|
|
||||||
} = postSlice.actions;
|
|
||||||
|
|
||||||
export default postSlice.reducer;
|
|
||||||
@@ -7,7 +7,7 @@ interface postState {
|
|||||||
post: Post | null;
|
post: Post | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: {};
|
error: {};
|
||||||
//Todo erros
|
//Todo errors
|
||||||
}
|
}
|
||||||
const initialState: postState = {
|
const initialState: postState = {
|
||||||
posts: [],
|
posts: [],
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
profile: null,
|
|
||||||
profiles: [],
|
|
||||||
repos: [],
|
|
||||||
loading: true,
|
|
||||||
error: {},
|
|
||||||
};
|
|
||||||
const profileSlice = createSlice({
|
|
||||||
name: "profile",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
getProfile(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
profile: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updateProfile(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
profile: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getProfilesType(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
profiles: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
profileError(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: action.payload,
|
|
||||||
loading: false,
|
|
||||||
profile: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
clearProfile(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
profile: null,
|
|
||||||
repos: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getRepos(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
repos: action.payload,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
noRepos(state, action) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
repos: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const {
|
|
||||||
noRepos,
|
|
||||||
getRepos,
|
|
||||||
clearProfile,
|
|
||||||
profileError,
|
|
||||||
getProfilesType,
|
|
||||||
updateProfile,
|
|
||||||
getProfile,
|
|
||||||
} = profileSlice.actions;
|
|
||||||
|
|
||||||
export default profileSlice.reducer;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import setAuthToken from './utils/setAuthToken';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import alertReducer from './reducers/alert';
|
|
||||||
import authReducer from './reducers/auth';
|
|
||||||
import profileReducer from './reducers/profile'
|
|
||||||
import postReducer from './reducers/post'
|
|
||||||
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
alert: alertReducer,
|
|
||||||
auth: authReducer,
|
|
||||||
profile: profileReducer,
|
|
||||||
post: postReducer
|
|
||||||
}})
|
|
||||||
|
|
||||||
let currentState = store.getState();
|
|
||||||
|
|
||||||
store.subscribe(() => {
|
|
||||||
// keep track of the previous and current state to compare changes
|
|
||||||
let previousState = currentState;
|
|
||||||
currentState = store.getState();
|
|
||||||
// if the token changes set the value in localStorage and axios headers
|
|
||||||
if (previousState.auth.token !== currentState.auth.token) {
|
|
||||||
const token = currentState.auth.token;
|
|
||||||
setAuthToken(token);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import store from '../store';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(res) => res,
|
|
||||||
(err) => {
|
|
||||||
if (err.response.status === 401) {
|
|
||||||
store.dispatch({ type: 'auth/logOut' });
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default api;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
function formatDate(date) {
|
|
||||||
return new Intl.DateTimeFormat().format(new Date(date));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default formatDate;
|
|
||||||
@@ -3,6 +3,6 @@ import type { TypedUseSelectorHook } from "react-redux";
|
|||||||
import type { RootState, AppDispatch } from "../store";
|
import type { RootState, AppDispatch } from "../store";
|
||||||
|
|
||||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
|
// taken from the redux wiki. Same as normal hook but typed
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
export const useAppDispatch2 = () => useDispatch<AppDispatch>;
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import api from './api';
|
|
||||||
|
|
||||||
// store our JWT in LS and set axios headers if we do have a token
|
|
||||||
|
|
||||||
const setAuthToken = (token) => {
|
|
||||||
if (token) {
|
|
||||||
api.defaults.headers.common['x-auth-token'] = token;
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
} else {
|
|
||||||
delete api.defaults.headers.common['x-auth-token'];
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setAuthToken;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: devcon-frontend-dev
|
image: devcon-frontend
|
||||||
build:
|
build:
|
||||||
context: ./client
|
context: ./client
|
||||||
dockerfile: ./dev.Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 5173:5173
|
- 5173:5173
|
||||||
environment:
|
environment:
|
||||||
@@ -12,10 +12,10 @@ services:
|
|||||||
- ./client:/usr/src/app
|
- ./client:/usr/src/app
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: devcon-backend-dev
|
image: devcon-backend
|
||||||
build:
|
build:
|
||||||
context: ./server
|
context: ./server
|
||||||
dockerfile: ./dev.Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
volumes:
|
volumes:
|
||||||
+8
-6
@@ -1,19 +1,21 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: devcon-frontend-dev
|
image: devcon-frontend
|
||||||
build:
|
build:
|
||||||
context: ./client
|
context: ./client
|
||||||
dockerfile: ./dev.Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 8080:80
|
||||||
|
environment:
|
||||||
|
VITE_BACKEND_URL: http://server:5000
|
||||||
volumes:
|
volumes:
|
||||||
- ./client:/usr/src/app
|
- ./client:/usr/src/app
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: devcon-backend-dev
|
image: devcon-backend
|
||||||
build:
|
build:
|
||||||
context: ./server
|
context: ./server
|
||||||
dockerfile: ./dev.Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
volumes:
|
volumes:
|
||||||
@@ -22,7 +24,7 @@ services:
|
|||||||
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
|
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
|
||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo
|
image: mongo:4.4
|
||||||
ports:
|
ports:
|
||||||
- 3456:27017
|
- 3456:27017
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -10,6 +10,3 @@ db.createUser({
|
|||||||
});
|
});
|
||||||
|
|
||||||
db.createCollection("todos");
|
db.createCollection("todos");
|
||||||
|
|
||||||
db.todos.insert({ text: "Write code", done: true });
|
|
||||||
db.todos.insert({ text: "Learn about containers", done: false });
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const connectDB= require('./config/db')
|
|
||||||
const path = require('path')
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
connectDB();
|
|
||||||
|
|
||||||
app.use(express.json({extended:false}));
|
|
||||||
|
|
||||||
|
|
||||||
app.use('/api/users', require('./routers/api/users'))
|
|
||||||
app.use('/api/auth', require('./routers/api/auth'))
|
|
||||||
app.use('/api/profile', require('./routers/api/profile'))
|
|
||||||
app.use('/api/posts', require('./routers/api/posts'))
|
|
||||||
// Serve static assets in production
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV==='production'){
|
|
||||||
app.use(express.static('client/build'));
|
|
||||||
app.get('*',(req, res)=>[
|
|
||||||
res.sendFile(path.resolve(__dirname, 'client', 'build','index.html'))
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5000;
|
|
||||||
|
|
||||||
app.listen(PORT,()=> console.log(`Server started on port ${PORT}`));
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class CreateComment : SecuredRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames = ["message", "post"];
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
bodyParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("comment", bodyParamNames));
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
if (!int.TryParse(paramsToValidate["post"], out int myInt) || myInt < 0)
|
||||||
|
throw new Exception("Incorect post");
|
||||||
|
}
|
||||||
|
if (paramsToValidate["message"].Length > 1000)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class CreateEducation : SecuredRoute
|
||||||
|
{
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
string format = "yyyy-MM-dd";
|
||||||
|
if (
|
||||||
|
paramsToValidate["school"].Length > 70
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["school"])
|
||||||
|
|| paramsToValidate["degree"].Length > 120
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["degree"])
|
||||||
|
|| paramsToValidate["field"].Length > 100
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["field"])
|
||||||
|
|| !DateTime.TryParseExact(
|
||||||
|
paramsToValidate["from_date"],
|
||||||
|
format,
|
||||||
|
null,
|
||||||
|
System.Globalization.DateTimeStyles.None,
|
||||||
|
out _
|
||||||
|
)
|
||||||
|
|| !DateTime.TryParseExact(
|
||||||
|
paramsToValidate["to_date"],
|
||||||
|
format,
|
||||||
|
null,
|
||||||
|
System.Globalization.DateTimeStyles.None,
|
||||||
|
out _
|
||||||
|
)
|
||||||
|
|| paramsToValidate["description"].Length > 1000
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames =
|
||||||
|
[
|
||||||
|
"school",
|
||||||
|
"degree",
|
||||||
|
"field",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"description",
|
||||||
|
];
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
bodyParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("education", bodyParamNames));
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class CreateExperience : SecuredRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames =
|
||||||
|
[
|
||||||
|
"job",
|
||||||
|
"company",
|
||||||
|
"location",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"description",
|
||||||
|
];
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
bodyParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("experience", bodyParamNames));
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
string format = "yyyy-MM-dd";
|
||||||
|
if (
|
||||||
|
paramsToValidate["job"].Length > 70
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["job"])
|
||||||
|
|| paramsToValidate["company"].Length > 120
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["company"])
|
||||||
|
|| paramsToValidate["location"].Length > 100
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["location"])
|
||||||
|
|| !DateTime.TryParseExact(
|
||||||
|
paramsToValidate["from_date"],
|
||||||
|
format,
|
||||||
|
null,
|
||||||
|
System.Globalization.DateTimeStyles.None,
|
||||||
|
out _
|
||||||
|
)
|
||||||
|
|| !DateTime.TryParseExact(
|
||||||
|
paramsToValidate["to_date"],
|
||||||
|
format,
|
||||||
|
null,
|
||||||
|
System.Globalization.DateTimeStyles.None,
|
||||||
|
out _
|
||||||
|
)
|
||||||
|
|| paramsToValidate["description"].Length > 1000
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class CreatePost : SecuredRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames = ["message"];
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
bodyParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("post", bodyParamNames));
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
if (paramsToValidate["message"].Length > 1000)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class CreateProfile : SecuredRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames =
|
||||||
|
[
|
||||||
|
"f_name",
|
||||||
|
"l_name",
|
||||||
|
"company",
|
||||||
|
"website",
|
||||||
|
"location",
|
||||||
|
"github",
|
||||||
|
"status",
|
||||||
|
"bio",
|
||||||
|
"skills",
|
||||||
|
"twitter",
|
||||||
|
"facebook",
|
||||||
|
"youtube",
|
||||||
|
"linkedin",
|
||||||
|
"instagram",
|
||||||
|
];
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
bodyParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("profile", bodyParamNames));
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
paramsToValidate["f_name"].Length > 30
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["f_name"])
|
||||||
|
|| paramsToValidate["l_name"].Length > 30
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["l_name"])
|
||||||
|
|| paramsToValidate["company"].Length > 70
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["company"])
|
||||||
|
|| paramsToValidate["website"].Length > 120
|
||||||
|
|| paramsToValidate["location"].Length > 100
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["location"])
|
||||||
|
|| paramsToValidate["skills"].Length > 300
|
||||||
|
|| paramsToValidate["github"].Length > 120
|
||||||
|
|| paramsToValidate["status"].Length > 20
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["status"])
|
||||||
|
|| paramsToValidate["bio"].Length > 1000
|
||||||
|
|| paramsToValidate["twitter"].Length > 100
|
||||||
|
|| paramsToValidate["facebook"].Length > 100
|
||||||
|
|| paramsToValidate["youtube"].Length > 100
|
||||||
|
|| paramsToValidate["linkedin"].Length > 100
|
||||||
|
|| paramsToValidate["instagram"].Length > 100
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeleteComment : DeleteRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteFromDB(request, "comment", ["id"], true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeleteEducation : DeleteRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteFromDB(request, "education", ["id"], true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeleteExperience : DeleteRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteFromDB(request, "education", ["id"], true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeletePost : DeleteRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteFromDB(request, "post", ["id"], true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeleteProfile : DeleteRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteFromDB(request, "profile", ["id"], false);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class DeleteRoute : SecuredRoute
|
||||||
|
{
|
||||||
|
protected static void DeleteFromDB(
|
||||||
|
HttpListenerRequest request,
|
||||||
|
string table,
|
||||||
|
List<string> validParamNames,
|
||||||
|
bool requireId
|
||||||
|
)
|
||||||
|
// TODO should return error when it cant find the comment
|
||||||
|
{
|
||||||
|
// extract userid compare userid to the comment userid
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, validParamNames);
|
||||||
|
|
||||||
|
if (requireId && bodyParamValues["id"] is null)
|
||||||
|
throw new Exception("missing id");
|
||||||
|
|
||||||
|
validParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
table += requireId ? " Where user_id=@user_id;" : " WHERE id=@id AND user_id=@user_id;";
|
||||||
|
MySqlCommand cmd = new("DELETE from " + table);
|
||||||
|
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
FROM node:20
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
|
|
||||||
COPY --chown=node:node . .
|
|
||||||
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
ENV DEBUG=express:*
|
|
||||||
|
|
||||||
|
|
||||||
USER node
|
|
||||||
|
|
||||||
CMD npm start
|
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class Login : SecuredRoute
|
||||||
|
{
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
string.IsNullOrEmpty(paramsToValidate["email"])
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["password"])
|
||||||
|
)
|
||||||
|
throw new Exception("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames = ["email", "password"];
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
string query =
|
||||||
|
@"SELECT id, password FROM user
|
||||||
|
WHERE email=@email;";
|
||||||
|
MySqlCommand cmd = new(query);
|
||||||
|
cmd.Parameters.AddWithValue("@email", bodyParamValues["email"]);
|
||||||
|
|
||||||
|
var userId = ExtractUserIdFromDB(cmd, bodyParamValues["password"]);
|
||||||
|
|
||||||
|
string? jsonResponse = JsonConvert.SerializeObject(GenerateToken(userId));
|
||||||
|
// prepare response
|
||||||
|
SendSuccess(response, jsonResponse);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractUserIdFromDB(MySqlCommand cmd, string password)
|
||||||
|
{
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
cmd.Connection = conn;
|
||||||
|
conn.Open();
|
||||||
|
// execute query and read results
|
||||||
|
MySqlDataReader reader = cmd.ExecuteReader();
|
||||||
|
string? userId = "";
|
||||||
|
string? hashedPass = "";
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
userId = Convert.ToString(reader["id"]);
|
||||||
|
hashedPass = reader.GetString("password");
|
||||||
|
}
|
||||||
|
// check username
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid Username or Password");
|
||||||
|
}
|
||||||
|
//check password
|
||||||
|
if (
|
||||||
|
string.IsNullOrEmpty(password)
|
||||||
|
|| string.IsNullOrEmpty(hashedPass)
|
||||||
|
|| !VerifyPassword(password, hashedPass)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid Username or Password");
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateToken(string user)
|
||||||
|
{
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: "TimeLogServer",
|
||||||
|
audience: "TimeLogWebsite",
|
||||||
|
claims: [new Claim("user", user)],
|
||||||
|
expires: DateTime.Now.AddHours(2),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool VerifyPassword(string enteredPassword, string storedHash)
|
||||||
|
{
|
||||||
|
byte[] hashBytes = Convert.FromBase64String(storedHash);
|
||||||
|
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
Array.Copy(hashBytes, 0, salt, 0, 16);
|
||||||
|
|
||||||
|
using var pbkdf2 = new Rfc2898DeriveBytes(
|
||||||
|
enteredPassword,
|
||||||
|
salt,
|
||||||
|
10000,
|
||||||
|
HashAlgorithmName.SHA256
|
||||||
|
);
|
||||||
|
byte[] newHash = pbkdf2.GetBytes(32);
|
||||||
|
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
if (newHash[i] != hashBytes[i + 16])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
// create server
|
||||||
|
HttpListener listener = new();
|
||||||
|
// routes need to be added first
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/login/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/register/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/posts/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/posts/like/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/posts/unlike/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/comment/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/profile/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/profile/experience/");
|
||||||
|
listener.Prefixes.Add("http://localhost:5000/api/profile/education/");
|
||||||
|
// listen
|
||||||
|
listener.Start();
|
||||||
|
Console.WriteLine("Server is listening on http://localhost:5000/");
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
HttpListenerContext context = listener.GetContext();
|
||||||
|
HttpListenerRequest request = context.Request;
|
||||||
|
HttpListenerResponse response = context.Response;
|
||||||
|
response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||||
|
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
|
||||||
|
// url after localhost:5000/
|
||||||
|
string uri;
|
||||||
|
if (request != null && request.Url != null)
|
||||||
|
uri = request.Url.AbsolutePath;
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
switch (request.HttpMethod)
|
||||||
|
{
|
||||||
|
case "GET":
|
||||||
|
HandleGet(uri, request, response);
|
||||||
|
break;
|
||||||
|
case "POST":
|
||||||
|
HandlePost(uri, request, response);
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
HandleDelete(uri, request, response);
|
||||||
|
break;
|
||||||
|
case "PUT":
|
||||||
|
HandlePut(uri, request, response);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
HandleMissingPath(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandlePut(
|
||||||
|
string uri,
|
||||||
|
HttpListenerRequest request,
|
||||||
|
HttpListenerResponse response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (request.HasEntityBody)
|
||||||
|
switch (uri)
|
||||||
|
{
|
||||||
|
case "/api/profile":
|
||||||
|
UpdateProfile.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/education":
|
||||||
|
UpdateEducation.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/experience":
|
||||||
|
UpdateExperience.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/post":
|
||||||
|
UpdatePost.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/comment":
|
||||||
|
UpdateComment.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/posts/like":
|
||||||
|
UpdatePost.HandleLikes(request, response);
|
||||||
|
break;
|
||||||
|
// case "/api/posts/unlike":
|
||||||
|
// RemoveLike.HandleRequest(request, response);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
HandleMissingPath(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleMissingPath(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleDelete(
|
||||||
|
string uri,
|
||||||
|
HttpListenerRequest request,
|
||||||
|
HttpListenerResponse response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (request.HasEntityBody)
|
||||||
|
switch (uri)
|
||||||
|
{
|
||||||
|
case "/api/profile":
|
||||||
|
DeleteProfile.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/profile/education":
|
||||||
|
DeleteEducation.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/profile/experience":
|
||||||
|
DeleteExperience.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/posts":
|
||||||
|
DeletePost.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/comment":
|
||||||
|
DeleteComment.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
HandleMissingPath(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleMissingPath(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandlePost(
|
||||||
|
string uri,
|
||||||
|
HttpListenerRequest request,
|
||||||
|
HttpListenerResponse response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (request.HasEntityBody)
|
||||||
|
switch (uri)
|
||||||
|
{
|
||||||
|
case "/api/profile":
|
||||||
|
CreateProfile.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/profile/education":
|
||||||
|
CreateEducation.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/profile/experience":
|
||||||
|
CreateExperience.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/posts":
|
||||||
|
CreatePost.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/comment":
|
||||||
|
CreateComment.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/register":
|
||||||
|
Register.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
case "/api/login":
|
||||||
|
Login.HandleRequest(request, response);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
HandleMissingPath(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleMissingPath(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleGet(
|
||||||
|
string uri,
|
||||||
|
HttpListenerRequest request,
|
||||||
|
HttpListenerResponse response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
switch (uri)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
HandleMissingPath(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleMissingPath(HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
response.StatusCode = 404;
|
||||||
|
string errorMessage = "Not Found";
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(errorMessage);
|
||||||
|
response.ContentType = "text/plain";
|
||||||
|
response.ContentLength64 = buffer.Length;
|
||||||
|
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||||
|
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class Register : SecuredRoute
|
||||||
|
{
|
||||||
|
private static void ValidateParams(Dictionary<string, string> paramsToValidate)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
string.IsNullOrEmpty(paramsToValidate["username"])
|
||||||
|
|| paramsToValidate["username"].Length > 30
|
||||||
|
|| paramsToValidate["username"].Length < 4
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["email"])
|
||||||
|
|| paramsToValidate["email"].Length > 50
|
||||||
|
|| paramsToValidate["email"].Length < 6
|
||||||
|
|| string.IsNullOrEmpty(paramsToValidate["password"])
|
||||||
|
|| paramsToValidate["password"].Length > 50
|
||||||
|
|| paramsToValidate["password"].Length < 10
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new Exception("Wrong parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> bodyParamNames = ["username", "email", "password"];
|
||||||
|
var bodyParamValues = ExtractBody(request, bodyParamNames);
|
||||||
|
ValidateParams(bodyParamValues);
|
||||||
|
|
||||||
|
MySqlCommand cmd = new(CreateInsertQuery("user", bodyParamNames));
|
||||||
|
bodyParamValues["password"] = HashPassword(bodyParamValues["password"]);
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashPassword(string password)
|
||||||
|
{
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
RandomNumberGenerator.Fill(salt);
|
||||||
|
|
||||||
|
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256);
|
||||||
|
byte[] hash = pbkdf2.GetBytes(32);
|
||||||
|
|
||||||
|
byte[] hashBytes = new byte[48]; // 16 (salt) + 32 (hash)
|
||||||
|
Array.Copy(salt, 0, hashBytes, 0, 16);
|
||||||
|
Array.Copy(hash, 0, hashBytes, 16, 32);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(hashBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public abstract class Route
|
||||||
|
{
|
||||||
|
public static readonly string connectionString =
|
||||||
|
"server=127.0.0.1;uid=monty;pwd=some_pass;database=devcon";
|
||||||
|
|
||||||
|
public static void SendError(HttpListenerResponse response, Exception ex)
|
||||||
|
{
|
||||||
|
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
string errorMessage = $"Error: {ex.Message}";
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(errorMessage);
|
||||||
|
response.ContentType = "text/plain";
|
||||||
|
response.ContentLength64 = buffer.Length;
|
||||||
|
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SendSuccess(HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
response.StatusCode = (int)HttpStatusCode.OK;
|
||||||
|
response.StatusDescription = "Status OK";
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SendSuccess(HttpListenerResponse response, string jsonResponse)
|
||||||
|
{
|
||||||
|
response.StatusCode = (int)HttpStatusCode.OK;
|
||||||
|
response.StatusDescription = "Status OK";
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(jsonResponse);
|
||||||
|
response.ContentType = "application/json";
|
||||||
|
response.ContentLength64 = buffer.Length;
|
||||||
|
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ValidateDate(string date)
|
||||||
|
{
|
||||||
|
Regex regex = new(@"^\d{4}-\d{2}-\d{2}$");
|
||||||
|
return regex.IsMatch(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Dictionary<string, string> ExtractBody(
|
||||||
|
HttpListenerRequest request,
|
||||||
|
List<string> allowedParams
|
||||||
|
)
|
||||||
|
{
|
||||||
|
using StreamReader bodyReader = new(request.InputStream, request.ContentEncoding);
|
||||||
|
JObject bodyJO = JObject.Parse(bodyReader.ReadToEnd());
|
||||||
|
|
||||||
|
Dictionary<string, string> bodyParamValues = [];
|
||||||
|
foreach (var prop in bodyJO.Properties())
|
||||||
|
{
|
||||||
|
if (allowedParams.Contains(prop.Name))
|
||||||
|
bodyParamValues[prop.Name] = prop.Value.ToString();
|
||||||
|
}
|
||||||
|
return bodyParamValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class SecuredRoute : Route
|
||||||
|
{
|
||||||
|
protected static readonly string secretKey =
|
||||||
|
"stronk-key-much-sercret-much-more-stronk-stronk-key-much-sercret-much-more-stronk";
|
||||||
|
protected delegate void DelegateValidate(Dictionary<string, string> bodyparamValues);
|
||||||
|
|
||||||
|
protected static string ExtractUserId(HttpListenerRequest request)
|
||||||
|
{
|
||||||
|
var headers = request.Headers;
|
||||||
|
string token = headers["token"] ?? "";
|
||||||
|
string? usernameClaim = GetUserFromToken(token);
|
||||||
|
if (
|
||||||
|
!string.IsNullOrEmpty(token)
|
||||||
|
&& !ValidateToken(token)
|
||||||
|
&& string.IsNullOrEmpty(usernameClaim)
|
||||||
|
)
|
||||||
|
return "";
|
||||||
|
else
|
||||||
|
return usernameClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static MySqlCommand AddValuesToCmd(
|
||||||
|
Dictionary<string, string> values,
|
||||||
|
MySqlCommand cmd
|
||||||
|
)
|
||||||
|
{
|
||||||
|
foreach (var item in values)
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@" + item.Key, item.Value);
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an insert route and move this func there
|
||||||
|
protected static string CreateInsertQuery(string table, List<string> valuesToAdd)
|
||||||
|
{
|
||||||
|
string query =
|
||||||
|
"INSERT INTO "
|
||||||
|
+ table
|
||||||
|
+ "("
|
||||||
|
+ string.Join(",", valuesToAdd)
|
||||||
|
+ ") VALUES(@"
|
||||||
|
+ string.Join(",@", valuesToAdd)
|
||||||
|
+ ");";
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ValidateToken(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var validationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidIssuer = "TimeLogServer",
|
||||||
|
ValidAudience = "TimeLogWebsite",
|
||||||
|
IssuerSigningKey = key,
|
||||||
|
};
|
||||||
|
|
||||||
|
var principal = tokenHandler.ValidateToken(
|
||||||
|
token,
|
||||||
|
validationParameters,
|
||||||
|
out SecurityToken validatedToken
|
||||||
|
);
|
||||||
|
return validatedToken != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUserFromToken(string token)
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwtToken = handler.ReadJwtToken(token);
|
||||||
|
string? usernameClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "user")?.Value;
|
||||||
|
return string.IsNullOrEmpty(usernameClaim) ? "" : usernameClaim;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
|
||||||
|
<PackageReference Include="MySql.Data" Version="9.1.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
GET api/auth/ -- get token
|
||||||
|
GET api/posts
|
||||||
|
GET api/posts/:id
|
||||||
|
GET api/profile/me
|
||||||
|
GET api/profile/user/:user_id
|
||||||
|
GET api/profile/github/:username
|
||||||
|
# PUT api/profile/experience
|
||||||
|
# PUT api/profile/education
|
||||||
|
# PUT api/posts/like/:id
|
||||||
|
# PUT api/posts/unlike/:id
|
||||||
|
POST api/users -- register user
|
||||||
|
POST api/auth/ -- login
|
||||||
|
# POST api/profile
|
||||||
|
# POST api/posts
|
||||||
|
# POST api/posts/comment/:id
|
||||||
|
DELETE api/profile -- delete everything the user has done
|
||||||
|
# DELETE api/profile/education/:exp_id
|
||||||
|
# DELETE api/posts/comment/:id/:comment_id
|
||||||
|
# DELETE api/profile/experience/:exp_id
|
||||||
|
# DELETE api/posts/:id
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateComment : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> validParamNames = ["message", "post", "id"];
|
||||||
|
|
||||||
|
UpdateDb(request, "comment", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LikeComment(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> validParamNames = ["likes", "id"];
|
||||||
|
|
||||||
|
UpdateDb(request, "comment", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateEducation : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames =
|
||||||
|
[
|
||||||
|
"school",
|
||||||
|
"degree",
|
||||||
|
"field",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"description",
|
||||||
|
"id",
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateDb(request, "education", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateExperience : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames =
|
||||||
|
[
|
||||||
|
"job",
|
||||||
|
"company",
|
||||||
|
"location",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"description",
|
||||||
|
"id",
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateDb(request, "experience", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateLikes : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames = ["id"];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateLikes(request, "post", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdatePost : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames =
|
||||||
|
[
|
||||||
|
"f_name",
|
||||||
|
"l_name",
|
||||||
|
"company",
|
||||||
|
"website",
|
||||||
|
"location",
|
||||||
|
"github",
|
||||||
|
"status",
|
||||||
|
"bio",
|
||||||
|
"skills",
|
||||||
|
"twitter",
|
||||||
|
"facebook",
|
||||||
|
"youtube",
|
||||||
|
"linkedin",
|
||||||
|
"instagram",
|
||||||
|
"id",
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateDb(request, "post", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HandleLikes(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames = ["id"];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateLikes(request, "post", validParamNames, true);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateProfile : UpdateRoute
|
||||||
|
{
|
||||||
|
public static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
List<string> validParamNames =
|
||||||
|
[
|
||||||
|
"f_name",
|
||||||
|
"l_name",
|
||||||
|
"company",
|
||||||
|
"website",
|
||||||
|
"location",
|
||||||
|
"github",
|
||||||
|
"status",
|
||||||
|
"bio",
|
||||||
|
"skills",
|
||||||
|
"twitter",
|
||||||
|
"facebook",
|
||||||
|
"youtube",
|
||||||
|
"linkedin",
|
||||||
|
"instagram",
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateDb(request, "profile", validParamNames, false);
|
||||||
|
SendSuccess(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(response, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Net;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class UpdateRoute : SecuredRoute
|
||||||
|
{
|
||||||
|
//TODO create editied time on field in db for comments and posts
|
||||||
|
//TODO all updates need validation and deletes
|
||||||
|
protected static void UpdateDb(
|
||||||
|
HttpListenerRequest request,
|
||||||
|
string table,
|
||||||
|
List<string> validParamNames,
|
||||||
|
bool requireId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
string user_id = ExtractUserId(request);
|
||||||
|
var bodyParamValues = ExtractBody(request, validParamNames);
|
||||||
|
|
||||||
|
if (requireId && bodyParamValues["id"] is null)
|
||||||
|
throw new Exception("missing id");
|
||||||
|
|
||||||
|
string temp = "";
|
||||||
|
foreach (var item in bodyParamValues)
|
||||||
|
{
|
||||||
|
temp += item.Key + "=\"" + item.Value + "\",";
|
||||||
|
}
|
||||||
|
// remove last chat from str
|
||||||
|
temp = temp[..^1];
|
||||||
|
|
||||||
|
validParamNames.Add("user_id");
|
||||||
|
bodyParamValues["user_id"] = user_id;
|
||||||
|
|
||||||
|
temp += requireId ? " WHERE user_id=@user_id AND id=@id;" : " WHERE user_id=@user_id;";
|
||||||
|
|
||||||
|
MySqlCommand cmd = new("UPDATE " + table + " SET " + temp);
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void UpdateLikes(
|
||||||
|
HttpListenerRequest request,
|
||||||
|
string table,
|
||||||
|
List<string> validParamNames,
|
||||||
|
bool requireId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var bodyParamValues = ExtractBody(request, validParamNames);
|
||||||
|
if (requireId && bodyParamValues["id"] is null)
|
||||||
|
throw new Exception("missing id");
|
||||||
|
|
||||||
|
string query = "SELECT likes from post Where id=@id;";
|
||||||
|
MySqlCommand cmd2 = new(query);
|
||||||
|
using MySqlConnection conn = new(connectionString);
|
||||||
|
cmd2.Connection = conn;
|
||||||
|
conn.Open();
|
||||||
|
cmd2.Parameters.AddWithValue("@id", bodyParamValues["id"]);
|
||||||
|
MySqlDataReader reader = cmd2.ExecuteReader();
|
||||||
|
string? id = "";
|
||||||
|
string? likes = "";
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
id = Convert.ToString(reader["id"]);
|
||||||
|
likes = Convert.ToString(reader["likes"]);
|
||||||
|
}
|
||||||
|
Console.WriteLine(id);
|
||||||
|
|
||||||
|
query = "Update post SET likes=2 where id=1;";
|
||||||
|
MySqlCommand cmd = new(query);
|
||||||
|
cmd = AddValuesToCmd(bodyParamValues, cmd);
|
||||||
|
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# register
|
||||||
|
curl -X POST localhost:5000/api/register
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"username":"tombo" ,
|
||||||
|
"password":"1234567890" ,
|
||||||
|
"email":"temp@mail.com"
|
||||||
|
}
|
||||||
|
# login
|
||||||
|
curl -X POST localhost:5000/api/login
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"password":"1234567890" ,
|
||||||
|
"email":"temp@mail.com"
|
||||||
|
}
|
||||||
|
# add profile
|
||||||
|
curl -X POST localhost:5000/api/profile
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"f_name":"somef_name" ,
|
||||||
|
"l_name":"somel_name" ,
|
||||||
|
"company":"somecompany" ,
|
||||||
|
"website":"somewebsite" ,
|
||||||
|
"location":"somelocation" ,
|
||||||
|
"skills":"someskills" ,
|
||||||
|
"github":"somegithub" ,
|
||||||
|
"status":"somestatus" ,
|
||||||
|
"bio":"somebio" ,
|
||||||
|
"twitter":"sometwitter" ,
|
||||||
|
"youtube":"someyoutube" ,
|
||||||
|
"facebook":"somefacebook" ,
|
||||||
|
"linkedin":"somelinkedi" ,
|
||||||
|
"instagram":"someinstagram"
|
||||||
|
}
|
||||||
|
|
||||||
|
#update profile
|
||||||
|
curl -X PUT localhost:5000/api/profile/update
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"f_name":"Rombo" ,
|
||||||
|
"l_name":"Tombo" ,
|
||||||
|
"website":"TOMBOBOBOBO"
|
||||||
|
}
|
||||||
|
# "company":"somecompany" ,
|
||||||
|
# "website":"somewebsite" ,
|
||||||
|
# "location":"somelocation" ,
|
||||||
|
# "skills":"someskills" ,
|
||||||
|
# "github":"somegithub" ,
|
||||||
|
# "status":"somestatus" ,
|
||||||
|
# "bio":"somebio" ,
|
||||||
|
# "twitter":"sometwitter" ,
|
||||||
|
# "youtube":"someyoutube" ,
|
||||||
|
# "facebook":"somefacebook" ,
|
||||||
|
# "linkedin":"somelinkedi" ,
|
||||||
|
# "instagram":"someinstagram"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# add education
|
||||||
|
curl -X POST localhost:5000/api/profile/education
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"school": "someschool" ,
|
||||||
|
"degree": "somedegree" ,
|
||||||
|
"field": "somefield" ,
|
||||||
|
"from_date": "2020-01-01" ,
|
||||||
|
"to_date": "2020-02-02" ,
|
||||||
|
"description": "somedescription"
|
||||||
|
}
|
||||||
|
#update education
|
||||||
|
curl -X PUT localhost:5000/api/profile/education
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"school": "TOOBOBOOB" ,
|
||||||
|
"degree": "somedegree" ,
|
||||||
|
"field": "somefield" ,
|
||||||
|
"from_date": "2020-01-01" ,
|
||||||
|
"to_date": "2020-02-02" ,
|
||||||
|
"description": "somedescription",
|
||||||
|
"id":"1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# add exp
|
||||||
|
curl -X POST localhost:5000/api/profile/experience
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"job":"somejob" ,
|
||||||
|
"company":"somecompany" ,
|
||||||
|
"location":"somelocation" ,
|
||||||
|
"from_date":"2020-01-01" ,
|
||||||
|
"to_date":"2020-02-02" ,
|
||||||
|
"description":"12312312312312312312"
|
||||||
|
}
|
||||||
|
|
||||||
|
# add post
|
||||||
|
curl -X POST localhost:5000/api/post
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"message":"lskadfjalsk;djf;laksdjf;lsa"
|
||||||
|
}
|
||||||
|
#update profile
|
||||||
|
curl -X PUT localhost:5000/api/post/like
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"id":"4"
|
||||||
|
}
|
||||||
|
|
||||||
|
# add comment
|
||||||
|
curl -X POST localhost:5000/api/comment
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"message":"lskadfjalsk;djf;laksdjf;lsa" ,
|
||||||
|
"post":"1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove comment
|
||||||
|
curl -X DELETE localhost:5000/api/comment
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"id":"2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove education
|
||||||
|
curl -X DELETE localhost:5000/api/profile/education
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"id":"2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove experience
|
||||||
|
curl -X DELETE localhost:5000/api/profile/experience
|
||||||
|
-H "token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSIsImV4cCI6MTczNDEyOTM5MSwiaXNzIjoiVGltZUxvZ1NlcnZlciIsImF1ZCI6IlRpbWVMb2dXZWJzaXRlIn0.TZQQUaMBhL3PO3BvwpANMCImCk_RxGg7B5rTcbs9gRg"
|
||||||
|
-d
|
||||||
|
{
|
||||||
|
"id":"2"
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
import config from "config";
|
|
||||||
|
|
||||||
const db = process.env.MONGO_URL
|
|
||||||
? process.env.MONGO_URL
|
|
||||||
: config.get("mongoURI");
|
|
||||||
|
|
||||||
const connectDB = async () => {
|
|
||||||
try {
|
|
||||||
if (typeof db === "string") await mongoose.connect(db);
|
|
||||||
|
|
||||||
console.log("MongoDB Connected...");
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (typeof err === "string") console.error(err);
|
|
||||||
else if (err instanceof Error) console.error(err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connectDB;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
FROM node:20
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY --chown=node:node . .
|
|
||||||
|
|
||||||
RUN npm i
|
|
||||||
|
|
||||||
ENV DEBUG=express:*
|
|
||||||
|
|
||||||
USER node
|
|
||||||
|
|
||||||
CMD npm start server
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo
|
|
||||||
ports:
|
|
||||||
- 3456:27017
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: root
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: example
|
|
||||||
MONGO_INITDB_DATABASE: the_database
|
|
||||||
volumes:
|
|
||||||
- ../mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
|
|
||||||
- mongo_data:/data/db
|
|
||||||
server: # The name of the service, can be anything
|
|
||||||
image: devcon-backend-dev # Declares which image to use
|
|
||||||
build: . # Declares where to build if image is not found
|
|
||||||
ports: # Declares the ports to publish
|
|
||||||
- 5000:5000
|
|
||||||
volumes:
|
|
||||||
- ./.:/usr/src/app
|
|
||||||
environment:
|
|
||||||
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo_data:
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
services:
|
|
||||||
app: # The name of the service, can be anything
|
|
||||||
image: devcon-backend # Declares which image to use
|
|
||||||
build: . # Declares where to build if image is not found
|
|
||||||
ports: # Declares the ports to publish
|
|
||||||
- 3000:3000
|
|
||||||
mongo:
|
|
||||||
image: mongo
|
|
||||||
ports:
|
|
||||||
- 3456:27017
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: root
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: example
|
|
||||||
MONGO_INITDB_DATABASE: the_database
|
|
||||||
volumes:
|
|
||||||
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
|
|
||||||
- mongo_data:/data/db
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo_data:
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import config from "config";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import type { Request, Response, NextFunction } from "express";
|
|
||||||
|
|
||||||
interface ResponseAndUser extends Request {
|
|
||||||
user?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function auth(req: ResponseAndUser, res: Response, next: NextFunction) {
|
|
||||||
// Get token from header
|
|
||||||
const token = req.header("x-auth-token");
|
|
||||||
// Check if not token
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ msg: "No token, authorization denied" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token
|
|
||||||
try {
|
|
||||||
jwt.verify(token, config.get("jwtSecret"), (error, decoded) => {
|
|
||||||
if (error) {
|
|
||||||
return res.status(401).json({ msg: "Token is not valid" });
|
|
||||||
} else {
|
|
||||||
if (decoded && typeof decoded !== "string") req.user = decoded?.user;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("something wrong with auth middleware");
|
|
||||||
res.status(500).json({ msg: "Server Error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default auth;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
// middleware to check for a valid object id
|
|
||||||
import type { Request, Response, NextFunction } from "express";
|
|
||||||
const checkObjectId = (idToCheck: string) => (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
if (!mongoose.Types.ObjectId.isValid(req.params[idToCheck]))
|
|
||||||
return res.status(400).json({ msg: 'Invalid ID' });
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default checkObjectId
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const PostSchema = new Schema({
|
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
likes: [
|
|
||||||
{
|
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
comments: [
|
|
||||||
{
|
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
date: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Post = mongoose.model("post", PostSchema);
|
|
||||||
export default Post
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
|
|
||||||
const ProfileSchema = new mongoose.Schema({
|
|
||||||
user: {
|
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
|
||||||
ref: "user",
|
|
||||||
},
|
|
||||||
company: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
website: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
skills: {
|
|
||||||
type: [String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
bio: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
githubusername: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
experience: [
|
|
||||||
{
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
company: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
type: Date,
|
|
||||||
},
|
|
||||||
current: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
education: [
|
|
||||||
{
|
|
||||||
school: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
degree: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
fieldofstudy: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
type: Date,
|
|
||||||
},
|
|
||||||
current: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
social: {
|
|
||||||
youtube: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
facebook: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
linkedin: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
instagram: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Profile = mongoose.model("profile", ProfileSchema);
|
|
||||||
export default Profile
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import mongoose from "mongoose";
|
|
||||||
|
|
||||||
const UserSchema = new mongoose.Schema({
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const User = mongoose.model("user", UserSchema);
|
|
||||||
|
|
||||||
export default User
|
|
||||||
Generated
-3676
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "devconnectts",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "server.ts",
|
|
||||||
"scripts": {
|
|
||||||
"start": "ts-node server.ts",
|
|
||||||
"server": "ts-node-dev server.ts",
|
|
||||||
"client": "npm start --prefix client --trace-depracation",
|
|
||||||
"dev": "concurrently \"npm run server\" \"npm run client\"",
|
|
||||||
"render": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"config": "^3.3.9",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"express-validator": "^7.0.1",
|
|
||||||
"gravatar": "^1.8.2",
|
|
||||||
"jsonwebtoken": "^9.0.1",
|
|
||||||
"mongoose": "^7.4.1",
|
|
||||||
"normalize-url": "^5.0.0",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"ts-node-dev": "^2.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/config": "^3.3.0",
|
|
||||||
"@types/express": "^4.17.17",
|
|
||||||
"@types/gravatar": "^1.8.3",
|
|
||||||
"@types/jsonwebtoken": "^9.0.2",
|
|
||||||
"@types/uuid": "^9.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const router = express.Router();
|
|
||||||
const bcrypt = require("bcryptjs");
|
|
||||||
const auth = require("../../middleware/auth");
|
|
||||||
const jwt = require("jsonwebtoken");
|
|
||||||
const config = require("config");
|
|
||||||
const { check, validationResult } = require("express-validator");
|
|
||||||
|
|
||||||
const User = require("../../models/User");
|
|
||||||
|
|
||||||
// @route GET api/auth
|
|
||||||
// @desc Get user by token
|
|
||||||
// @access Private
|
|
||||||
router.get("/", auth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const user = await User.findById(req.user.id).select("-password");
|
|
||||||
res.json(user);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err.message);
|
|
||||||
res.status(500).send("Server Error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// @route POST api/auth
|
|
||||||
// @desc Authenticate user & get token
|
|
||||||
// @access Public
|
|
||||||
router.post(
|
|
||||||
"/",
|
|
||||||
[
|
|
||||||
check("email", "Please include a valid email").isEmail(),
|
|
||||||
check("password", "Password is required").exists(),
|
|
||||||
],
|
|
||||||
async (req, res) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, password } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, user.password);
|
|
||||||
|
|
||||||
if (!isMatch) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
jwt.sign(
|
|
||||||
payload,
|
|
||||||
config.get("jwtSecret"),
|
|
||||||
{ expiresIn: 360000 },
|
|
||||||
(err, token) => {
|
|
||||||
if (err) throw err;
|
|
||||||
res.json({ token });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err.message);
|
|
||||||
res.status(500).send("Server error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import bcrypt from 'bcryptjs'
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
import auth from '../../middleware/auth'
|
|
||||||
import config from 'config'
|
|
||||||
import { check, validationResult } from "express-validator";
|
|
||||||
|
|
||||||
import User from '../../models/User'
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import { isUserId } from '../../utils';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
// @route GET api/auth
|
|
||||||
// @desc Get user by token
|
|
||||||
// @access Private
|
|
||||||
router.get("/", auth, async (req: any, res) => {
|
|
||||||
try {
|
|
||||||
let user: unknown = null
|
|
||||||
if (isUserId(req)) {
|
|
||||||
user = await User.findById(req.user.id).select("-password");
|
|
||||||
res.json(user);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error('missing id in request')
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (typeof err === 'string')
|
|
||||||
console.error(err)
|
|
||||||
else if (err instanceof Error)
|
|
||||||
console.error(err.message);
|
|
||||||
res.status(500).send("Server Error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// @route POST api/auth
|
|
||||||
// @desc Authenticate user & get token
|
|
||||||
// @access Public
|
|
||||||
router.post(
|
|
||||||
"/",
|
|
||||||
[
|
|
||||||
check("email", "Please include a valid email").isEmail(),
|
|
||||||
check("password", "Password is required").exists(),
|
|
||||||
],
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, password } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, user.password);
|
|
||||||
|
|
||||||
if (!isMatch) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const jwtSecret = config.get('jwtSecret')
|
|
||||||
if (typeof jwtSecret === 'string') jwt.sign(
|
|
||||||
payload,
|
|
||||||
jwtSecret,
|
|
||||||
{ expiresIn: 360000 },
|
|
||||||
(err, token) => {
|
|
||||||
if (err) throw err;
|
|
||||||
res.json({ token });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
else throw new Error('Error signing the jwt token')
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (typeof err === 'string')
|
|
||||||
console.error(err)
|
|
||||||
else if (err instanceof Error)
|
|
||||||
console.error(err.message);
|
|
||||||
res.status(500).send("Server error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user