Compare commits

..

11 Commits

Author SHA1 Message Date
QkoSad e11af3ad2a fighting weird bug with branches 2025-07-12 10:50:44 +03:00
QkoSad d0ea12ff8c initial dotnet server 2025-07-01 14:32:32 +03:00
QkoSad 3ba8c2abd5 fixed problems with docker that only existed on other systems 2025-06-17 16:53:54 +03:00
QkoSad 5a2b291bdd no idea what i did 2025-06-16 17:44:13 +03:00
Andrean 54da6e1bf5 Update README.md 2024-09-30 18:28:14 +00:00
QkoSad 27c1741bc0 fixed a docker issue updated the README 2024-09-30 21:26:58 +03:00
QkoSad da43bec842 Removed all of the duplicate js files from the rebase for github 2024-09-29 00:11:08 +03:00
QkoSad 1b88aa7e56 removed duplicate files 2024-09-29 00:00:58 +03:00
QkoSad 04b51ae321 minor change 2024-09-29 00:00:52 +03:00
QkoSad bcbffad32d fixed website breaking the server 2024-09-29 00:00:52 +03:00
QkoSad 46f6280152 Fix an issue with profile form 2024-09-29 00:00:52 +03:00
108 changed files with 1526 additions and 7773 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
node_modules
App.css
start_in_tmux.sh
default.json
production.json
bin
obj
+19
View File
@@ -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
-23
View File
@@ -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*
-2
View File
@@ -1,5 +1,3 @@
# The first FROM is now a stage called build-stage
FROM node:20 AS build-stage
WORKDIR /usr/src/app
-11
View File
@@ -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
-11
View File
@@ -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
+15 -8
View File
@@ -13,7 +13,7 @@
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@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/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
@@ -142,7 +142,7 @@
"@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.0"
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
@@ -175,6 +175,9 @@
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
@@ -1656,10 +1659,10 @@
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "^5.0.0",
"aria-query": "5.1.3",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
@@ -1809,7 +1812,7 @@
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.3.0"
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/estree": {
@@ -1835,8 +1838,8 @@
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
"license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
"jest-matcher-utils": "^27.0.0",
"pretty-format": "^27.0.0"
}
},
"node_modules/@types/parse-json": {
@@ -2027,6 +2030,10 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
@@ -3527,7 +3534,7 @@
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4"
"redux": "^4 || ^5.0.0-beta.0"
},
"peerDependenciesMeta": {
"@types/react": {
-99
View File
@@ -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;
-5
View File
@@ -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
-9
View File
@@ -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);
};
-68
View File
@@ -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);
};
-124
View File
@@ -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 })
);
}
};
-210
View File
@@ -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,
})
);
}
}
};
-61
View File
@@ -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;
-91
View File
@@ -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;
-4
View File
@@ -59,7 +59,6 @@ export default function SignUp() {
name="name"
required
fullWidth
id="name"
label="Name"
autoFocus
/>
@@ -68,7 +67,6 @@ export default function SignUp() {
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
/>
@@ -80,7 +78,6 @@ export default function SignUp() {
name="password"
label="Password"
type="password"
id="password"
/>
</Grid>
<Grid size={{ xs: 12 }}>
@@ -90,7 +87,6 @@ export default function SignUp() {
name="password2"
label="Repeat Password"
type="password"
id="password2"
/>
</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;
-16
View File
@@ -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;
-34
View File
@@ -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;
-57
View File
@@ -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 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import { Link } from "react-router-dom";
import { logOut } from "../../reducers/auth";
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
@@ -69,7 +68,7 @@ const Navbar = () => {
<AppBar position="static">
<Container maxWidth="xl">
<Toolbar disableGutters>
{/*Icon for big dislay*/}
{/*Icon for big display*/}
<AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
{/*LOGO link for bi display*/}
<Typography
-14
View File
@@ -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;
-14
View File
@@ -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;
-39
View File
@@ -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
-38
View File
@@ -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;
-39
View File
@@ -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;
-36
View File
@@ -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;
-57
View File
@@ -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;
-34
View File
@@ -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: "",
website: "",
location: "",
status: "",
skills: "",
githubusername: "",
bio: "",
status: "",
twitter: "",
facebook: "",
linkedin: "",
@@ -55,7 +55,7 @@ const ProfileForm = () => {
// then build our profileData
if (!loading && profile) {
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) {
if (key in profileData) profileData[key] = profile[key];
}
@@ -82,16 +82,19 @@ const ProfileForm = () => {
youtube,
linkedin,
instagram,
status,
} = formData;
const onChange = (
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 editing = profile ? true : false;
e.preventDefault();
const form = new FormData(e.currentTarget);
const status = form.get("status") as string;
if (facebook?.length > 100) {
dispatch(createAlert("Facebook link is longer 100 characters", "danger"));
} else if (linkedin?.length > 100) {
@@ -108,9 +111,9 @@ const ProfileForm = () => {
dispatch(createAlert("Skills is longer 100 characters", "danger"));
} else if (skills.length === 0) {
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) {
dispatch(createAlert("Status is required", "danger"));
} else if (location?.length > 100) {
dispatch(createAlert("Location is longer 100 characters", "danger"));
} else if (githubusername?.length > 50) {
dispatch(
@@ -212,12 +215,12 @@ const ProfileForm = () => {
</Grid>
<Grid size={{ xs: 6 }}>
<Typography>
Please use comma separeted values (eg. HTML, CSS, JavaScript, PHP)
Please use comma separated values (eg. HTML, CSS, JavaScript, PHP)
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
name="githubUser"
name="githubusername"
fullWidth
label="Github Username"
onChange={onChange}
@@ -237,9 +240,8 @@ const ProfileForm = () => {
labelId="status"
name="status"
required
placeholder="Select Profesional status"
placeholder="Select Professional status"
defaultValue={""}
value={status}
>
<MenuItem value="">None</MenuItem>
<MenuItem value="Developer">Developer</MenuItem>
@@ -256,7 +258,7 @@ const ProfileForm = () => {
</Grid>
<Grid size={{ xs: 6 }}>
<Typography paddingTop="2.3rem">
Select Profesional Status
Select Professional Status
</Typography>
</Grid>
<Grid size={{ xs: 12 }}>
-86
View File
@@ -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;
-9
View File
@@ -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/>)
-20
View File
@@ -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;
-76
View File
@@ -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;
-92
View File
@@ -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;
+1 -1
View File
@@ -7,7 +7,7 @@ interface postState {
post: Post | null;
loading: boolean;
error: {};
//Todo erros
//Todo errors
}
const initialState: postState = {
posts: [],
-75
View File
@@ -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;
-32
View File
@@ -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;
-20
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
function formatDate(date) {
return new Intl.DateTimeFormat().format(new Date(date));
}
export default formatDate;
+1 -1
View File
@@ -3,6 +3,6 @@ import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "../store";
// 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 useAppDispatch2 = () => useDispatch<AppDispatch>;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
-15
View File
@@ -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:
app:
image: devcon-frontend-dev
image: devcon-frontend
build:
context: ./client
dockerfile: ./dev.Dockerfile
dockerfile: ./Dockerfile
ports:
- 5173:5173
environment:
@@ -12,10 +12,10 @@ services:
- ./client:/usr/src/app
server:
image: devcon-backend-dev
image: devcon-backend
build:
context: ./server
dockerfile: ./dev.Dockerfile
dockerfile: ./Dockerfile
ports:
- 5000:5000
volumes:
+8 -6
View File
@@ -1,19 +1,21 @@
services:
app:
image: devcon-frontend-dev
image: devcon-frontend
build:
context: ./client
dockerfile: ./dev.Dockerfile
dockerfile: ./Dockerfile
ports:
- 3000:3000
- 8080:80
environment:
VITE_BACKEND_URL: http://server:5000
volumes:
- ./client:/usr/src/app
server:
image: devcon-backend-dev
image: devcon-backend
build:
context: ./server
dockerfile: ./dev.Dockerfile
dockerfile: ./Dockerfile
ports:
- 5000:5000
volumes:
@@ -22,7 +24,7 @@ services:
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
mongo:
image: mongo
image: mongo:4.4
ports:
- 3456:27017
environment:
-3
View File
@@ -10,6 +10,3 @@ db.createUser({
});
db.createCollection("todos");
db.todos.insert({ text: "Write code", done: true });
db.todos.insert({ text: "Learn about containers", done: false });
-27
View File
@@ -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}`));
+48
View File
@@ -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");
}
}
}
+75
View File
@@ -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);
}
}
}
+75
View File
@@ -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");
}
}
}
+44
View File
@@ -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");
}
}
}
+80
View File
@@ -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");
}
}
}
+19
View File
@@ -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);
}
}
}
+19
View File
@@ -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);
}
}
}
+19
View File
@@ -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);
}
}
}
+19
View File
@@ -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);
}
}
}
+19
View File
@@ -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);
}
}
}
+35
View File
@@ -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();
}
}
-15
View File
@@ -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
View File
@@ -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;
}
}
+199
View File
@@ -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);
}
}
+65
View File
@@ -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);
}
}
+64
View File
@@ -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;
}
}
+92
View File
@@ -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;
}
}
+17
View File
@@ -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>
+20
View File
@@ -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
+36
View File
@@ -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);
}
}
}
+30
View File
@@ -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);
}
}
}
+30
View File
@@ -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);
}
}
}
+21
View File
@@ -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);
}
}
}
+53
View File
@@ -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);
}
}
}
+37
View File
@@ -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);
}
}
}
+79
View File
@@ -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();
}
}
+145
View File
@@ -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"
}
-20
View File
@@ -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;
-13
View File
@@ -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
-25
View File
@@ -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:
-20
View File
@@ -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:
-32
View File
@@ -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;
-10
View File
@@ -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
-54
View File
@@ -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
-114
View File
@@ -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
-28
View File
@@ -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
-3676
View File
File diff suppressed because it is too large Load Diff
-38
View File
@@ -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"
}
}
-80
View File
@@ -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;
-96
View File
@@ -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