Repository: shamahoque/mern-mediastream
Branch: second-edition
Commit: bd167b515d98
Files: 50
Total size: 85.5 KB
Directory structure:
gitextract_hdvuq4ke/
├── .babelrc
├── .github/
│ └── stale.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── client/
│ ├── App.js
│ ├── MainRouter.js
│ ├── auth/
│ │ ├── PrivateRoute.js
│ │ ├── Signin.js
│ │ ├── api-auth.js
│ │ └── auth-helper.js
│ ├── core/
│ │ ├── Home.js
│ │ └── Menu.js
│ ├── main.js
│ ├── media/
│ │ ├── DeleteMedia.js
│ │ ├── EditMedia.js
│ │ ├── Media.js
│ │ ├── MediaList.js
│ │ ├── MediaPlayer.js
│ │ ├── NewMedia.js
│ │ ├── PlayMedia.js
│ │ ├── RelatedMedia.js
│ │ └── api-media.js
│ ├── routeConfig.js
│ ├── theme.js
│ └── user/
│ ├── DeleteUser.js
│ ├── EditProfile.js
│ ├── Profile.js
│ ├── Signup.js
│ ├── Users.js
│ └── api-user.js
├── config/
│ └── config.js
├── nodemon.json
├── package.json
├── server/
│ ├── controllers/
│ │ ├── auth.controller.js
│ │ ├── media.controller.js
│ │ └── user.controller.js
│ ├── devBundle.js
│ ├── express.js
│ ├── helpers/
│ │ └── dbErrorHandler.js
│ ├── models/
│ │ ├── media.model.js
│ │ └── user.model.js
│ ├── routes/
│ │ ├── auth.routes.js
│ │ ├── media.routes.js
│ │ └── user.routes.js
│ └── server.js
├── template.js
├── webpack.config.client.js
├── webpack.config.client.production.js
└── webpack.config.server.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel"
]
}
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: inactive
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
================================================
FILE: .gitignore
================================================
/node_modules/
/dist/
/data/
npm-debug.log
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2018 Shama Hoque
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# MERN Mediastream 2.0
- *Looking for the first edition code? [Check here](https://github.com/shamahoque/mern-mediastream/tree/master)*
A media streaming application with media upload and stream features - developed using React, Node, Express and MongoDB.

### [Live Demo](http://mediastream2.mernbook.com/ "MERN Mediastream")
#### What you need to run this code
1. Node (13.12.0)
2. NPM (6.14.4) or Yarn (1.22.4)
3. MongoDB (4.2.0)
#### How to run this code
1. Clone this repository
2. Open command line in the cloned folder,
- To install dependencies, run ``` npm install ``` or ``` yarn ```
- To run the application for development, run ``` npm run development ``` or ``` yarn development ```
4. Open [localhost:3000](http://localhost:3000/) in the browser
----
### More applications built using this stack
* [MERN Skeleton](https://github.com/shamahoque/mern-social/tree/second-edition)
* [MERN Social](https://github.com/shamahoque/mern-social/tree/second-edition)
* [MERN Classroom](https://github.com/shamahoque/mern-classroom)
* [MERN Marketplace](https://github.com/shamahoque/mern-marketplace/tree/second-edition)
* [MERN Expense Tracker](https://github.com/shamahoque/mern-expense-tracker)
* [MERN VR Game](https://github.com/shamahoque/mern-vrgame/tree/second-edition)
Learn more at [mernbook.com](http://www.mernbook.com/)
----
## Get the book
#### [Full-Stack React Projects - Second Edition](https://www.packtpub.com/web-development/full-stack-react-projects-second-edition)
*Learn MERN stack development by building modern web apps using MongoDB, Express, React, and Node.js*
<a href="https://www.packtpub.com/web-development/full-stack-react-projects-second-edition"><img src="https://mernbook.s3.amazonaws.com/git+/Book_2Ed.jpg" align="center" width="400" alt="Full-Stack React Projects"></a>
React combined with industry-tested, server-side technologies, such as Node, Express, and MongoDB, enables you to develop and deploy robust real-world full-stack web apps. This updated second edition focuses on the latest versions and conventions of the technologies in this stack, along with their new features such as Hooks in React and async/await in JavaScript. The book also explores advanced topics such as implementing real-time bidding, a web-based classroom app, and data visualization in an expense tracking app.
Full-Stack React Projects will take you through the process of preparing the development environment for MERN stack-based web development, creating a basic skeleton app, and extending it to build six different web apps. You'll build apps for social media, classrooms, media streaming, online marketplaces with real-time bidding, and web-based games with virtual reality features. Throughout the book, you'll learn how MERN stack web development works, extend its capabilities for complex features, and gain actionable insights into creating MERN-based apps, along with exploring industry best practices to meet the ever-increasing demands of the real world.
Things you'll learn in this book:
- Extend a MERN-based application to build a variety of applications
- Add real-time communication capabilities with Socket.IO
- Implement data visualization features for React applications using Victory
- Develop media streaming applications using MongoDB GridFS
- Improve SEO for your MERN apps by implementing server-side rendering with data
- Implement user authentication and authorization using JSON web tokens
- Set up and use React 360 to develop user interfaces with VR capabilities
- Make your MERN stack applications reliable and scalable with industry best practices
If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1839215410) today!
---
================================================
FILE: client/App.js
================================================
import React from 'react'
import MainRouter from './MainRouter'
import {BrowserRouter} from 'react-router-dom'
import { ThemeProvider } from '@material-ui/styles'
import theme from './theme'
import { hot } from 'react-hot-loader'
const App = () => {
React.useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles)
}
}, [])
return (
<BrowserRouter>
<ThemeProvider theme={theme}>
<MainRouter/>
</ThemeProvider>
</BrowserRouter>
)}
export default hot(module)(App)
================================================
FILE: client/MainRouter.js
================================================
import React, {Component} from 'react'
import {Route, Switch} from 'react-router-dom'
import Home from './core/Home'
import Users from './user/Users'
import Signup from './user/Signup'
import Signin from './auth/Signin'
import EditProfile from './user/EditProfile'
import Profile from './user/Profile'
import PrivateRoute from './auth/PrivateRoute'
import Menu from './core/Menu'
import NewMedia from './media/NewMedia'
import PlayMedia from './media/PlayMedia'
import EditMedia from './media/EditMedia'
const MainRouter = ({data}) => {
return (<div>
<Menu/>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/users" component={Users}/>
<Route path="/signup" component={Signup}/>
<Route path="/signin" component={Signin}/>
<PrivateRoute path="/user/edit/:userId" component={EditProfile}/>
<Route path="/user/:userId" component={Profile}/>
<PrivateRoute path="/media/new" component={NewMedia}/>
<PrivateRoute path="/media/edit/:mediaId" component={EditMedia}/>
<Route path="/media/:mediaId" render={(props) => (
<PlayMedia {...props} data={data} />
)} />
</Switch>
</div>)
}
export default MainRouter
================================================
FILE: client/auth/PrivateRoute.js
================================================
import React, { Component } from 'react'
import { Route, Redirect } from 'react-router-dom'
import auth from './auth-helper'
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
auth.isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/signin',
state: { from: props.location }
}}/>
)
)}/>
)
export default PrivateRoute
================================================
FILE: client/auth/Signin.js
================================================
import React, {useState} from 'react'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import Icon from '@material-ui/core/Icon'
import { makeStyles } from '@material-ui/core/styles'
import auth from './../auth/auth-helper'
import {Redirect} from 'react-router-dom'
import {signin} from './api-auth.js'
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
margin: 'auto',
textAlign: 'center',
marginTop: theme.spacing(5),
paddingBottom: theme.spacing(2)
},
error: {
verticalAlign: 'middle'
},
title: {
marginTop: theme.spacing(2),
color: theme.palette.openTitle
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing(2)
}
}))
export default function Signin(props) {
const classes = useStyles()
const [values, setValues] = useState({
email: '',
password: '',
error: '',
redirectToReferrer: false
})
const clickSubmit = () => {
const user = {
email: values.email || undefined,
password: values.password || undefined
}
signin(user).then((data) => {
if (data.error) {
setValues({ ...values, error: data.error})
} else {
auth.authenticate(data, () => {
setValues({ ...values, error: '',redirectToReferrer: true})
})
}
})
}
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value })
}
const {from} = props.location.state || {
from: {
pathname: '/'
}
}
const {redirectToReferrer} = values
if (redirectToReferrer) {
return (<Redirect to={from}/>)
}
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="h6" className={classes.title}>
Sign In
</Typography>
<TextField id="email" type="email" label="Email" className={classes.textField} value={values.email} onChange={handleChange('email')} margin="normal"/><br/>
<TextField id="password" type="password" label="Password" className={classes.textField} value={values.password} onChange={handleChange('password')} margin="normal"/>
<br/> {
values.error && (<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{values.error}
</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit} className={classes.submit}>Submit</Button>
</CardActions>
</Card>
)
}
================================================
FILE: client/auth/api-auth.js
================================================
const signin = async (user) => {
try {
let response = await fetch('/auth/signin/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const signout = async () => {
try {
let response = await fetch('/auth/signout/', { method: 'GET' })
return await response.json()
} catch(err) {
console.log(err)
}
}
export {
signin,
signout
}
================================================
FILE: client/auth/auth-helper.js
================================================
import { signout } from './api-auth.js'
const auth = {
isAuthenticated() {
if (typeof window == "undefined")
return false
if (sessionStorage.getItem('jwt'))
return JSON.parse(sessionStorage.getItem('jwt'))
else
return false
},
authenticate(jwt, cb) {
if (typeof window !== "undefined")
sessionStorage.setItem('jwt', JSON.stringify(jwt))
cb()
},
clearJWT(cb) {
if (typeof window !== "undefined")
sessionStorage.removeItem('jwt')
cb()
//optional
signout().then((data) => {
document.cookie = "t=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"
})
}
}
export default auth
================================================
FILE: client/core/Home.js
================================================
import React, {useState, useEffect} from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Card from '@material-ui/core/Card'
import Typography from '@material-ui/core/Typography'
import MediaList from '../media/MediaList'
import {listPopular} from '../media/api-media.js'
const useStyles = makeStyles(theme => ({
card: {
margin: `${theme.spacing(5)}px 30px`
},
title: {
padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px 0px`,
color: theme.palette.text.secondary,
fontSize: '1em'
},
media: {
minHeight: 330
}
}))
export default function Home(){
const classes = useStyles()
const [media, setMedia] = useState([])
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
listPopular(signal).then((data) => {
if (data.error) {
console.log(data.error)
} else {
setMedia(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [])
return (
<Card className={classes.card}>
<Typography variant="h2" className={classes.title}>
Popular Videos
</Typography>
<MediaList media={media}/>
</Card>
)
}
================================================
FILE: client/core/Menu.js
================================================
import React from 'react'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import HomeIcon from '@material-ui/icons/Home'
import AddBoxIcon from '@material-ui/icons/AddBox'
import Button from '@material-ui/core/Button'
import auth from './../auth/auth-helper'
import {Link, withRouter} from 'react-router-dom'
const isActive = (history, path) => {
if (history.location.pathname == path)
return {color: '#f99085'}
else
return {color: '#efdcd5'}
}
const Menu = withRouter(({history}) => (
<AppBar position="static">
<Toolbar>
<Typography type="title" color="inherit">
MERN Mediastream
</Typography>
<div>
<Link to="/">
<IconButton aria-label="Home" style={isActive(history, "/")}>
<HomeIcon/>
</IconButton>
</Link>
</div>
<div style={{'position':'absolute', 'right': '10px'}}><span style={{'float': 'right'}}>
{
!auth.isAuthenticated() && (<span>
<Link to="/signup">
<Button style={isActive(history, "/signup")}>Sign up
</Button>
</Link>
<Link to="/signin">
<Button style={isActive(history, "/signin")}>Sign In
</Button>
</Link>
</span>)
}
{
auth.isAuthenticated() && (<span>
<Link to="/media/new">
<Button style={isActive(history, "/media/new")}>
<AddBoxIcon style={{marginRight: '8px'}}/> Add Media
</Button>
</Link>
<Link to={"/user/" + auth.isAuthenticated().user._id}>
<Button style={isActive(history, "/user/" + auth.isAuthenticated().user._id)}>My Profile</Button>
</Link>
<Button color="inherit" onClick={() => {
auth.signout(() => history.push('/'))
}}>Sign out</Button>
</span>)
}
</span></div>
</Toolbar>
</AppBar>
))
export default Menu
================================================
FILE: client/main.js
================================================
import React from 'react'
import { hydrate } from 'react-dom'
import App from './App'
hydrate(<App/>, document.getElementById('root'))
================================================
FILE: client/media/DeleteMedia.js
================================================
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import IconButton from '@material-ui/core/IconButton'
import Button from '@material-ui/core/Button'
import DeleteIcon from '@material-ui/icons/Delete'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import auth from './../auth/auth-helper'
import {remove} from './api-media.js'
import {Redirect} from 'react-router-dom'
export default function DeleteMedia(props) {
const [open, setOpen] = useState(false)
const [redirect, setRedirect] = useState(false)
const jwt = auth.isAuthenticated()
const clickButton = () => {
setOpen(true)
}
const deleteMedia = () => {
const jwt = auth.isAuthenticated()
remove({
mediaId: props.mediaId
}, {t: jwt.token}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
setRedirect(true)
}
})
}
const handleRequestClose = () => {
setOpen(false)
}
if (redirect) {
return <Redirect to='/'/>
}
return (<span>
<IconButton aria-label="Delete" onClick={clickButton} color="secondary">
<DeleteIcon/>
</IconButton>
<Dialog open={open} onClose={handleRequestClose}>
<DialogTitle>{"Delete "+props.mediaTitle}</DialogTitle>
<DialogContent>
<DialogContentText>
Confirm to delete {props.mediaTitle} from your account.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleRequestClose} color="primary">
Cancel
</Button>
<Button onClick={deleteMedia} variant="contained" color="secondary" autoFocus="autoFocus">
Confirm
</Button>
</DialogActions>
</Dialog>
</span>)
}
DeleteMedia.propTypes = {
mediaId: PropTypes.string.isRequired,
mediaTitle: PropTypes.string.isRequired
}
================================================
FILE: client/media/EditMedia.js
================================================
import React, {useState, useEffect} from 'react'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import Icon from '@material-ui/core/Icon'
import { makeStyles } from '@material-ui/core/styles'
import auth from './../auth/auth-helper'
import {read, update} from './api-media.js'
import {Redirect} from 'react-router-dom'
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 500,
margin: 'auto',
textAlign: 'center',
marginTop: theme.spacing(5),
paddingBottom: theme.spacing(2)
},
title: {
margin: theme.spacing(2),
color: theme.palette.protectedTitle,
fontSize: '1em'
},
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing(2)
},
input: {
display: 'none'
},
filename:{
marginLeft:'10px'
}
}))
export default function EditProfile({ match }) {
const classes = useStyles()
const [media, setMedia] = useState({title: '', description:'', genre:''})
const [redirect, setRedirect] = useState(false)
const [error, setError] = useState('')
const jwt = auth.isAuthenticated()
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
read({mediaId: match.params.mediaId}).then((data) => {
if (data.error) {
setError(data.error)
} else {
setMedia(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [match.params.mediaId])
const clickSubmit = () => {
const jwt = auth.isAuthenticated()
update({
mediaId: media._id
}, {
t: jwt.token
}, media).then((data) => {
if (data.error) {
setError(data.error)
} else {
setRedirect(true)
}
})
}
const handleChange = name => event => {
let updatedMedia = {...media}
updatedMedia[name] = event.target.value
setMedia(updatedMedia)
}
if (redirect) {
return (<Redirect to={'/media/' + media._id}/>)
}
return (
<Card className={classes.card}>
<CardContent>
<Typography type="headline" component="h1" className={classes.title}>
Edit Video Details
</Typography>
<TextField id="title" label="Title" className={classes.textField} value={media.title} onChange={handleChange('title')} margin="normal"/><br/>
<TextField
id="multiline-flexible"
label="Description"
multiline
rows="2"
value={media.description}
onChange={handleChange('description')}
className={classes.textField}
margin="normal"
/><br/>
<TextField id="genre" label="Genre" className={classes.textField} value={media.genre} onChange={handleChange('genre')} margin="normal"/><br/>
<br/> {
error &&
(<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{error}
</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit} className={classes.submit}>Submit</Button>
</CardActions>
</Card>
)
}
================================================
FILE: client/media/Media.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import Card from '@material-ui/core/Card'
import CardHeader from '@material-ui/core/CardHeader'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemAvatar from '@material-ui/core/ListItemAvatar'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import IconButton from '@material-ui/core/IconButton'
import Edit from '@material-ui/icons/Edit'
import Avatar from '@material-ui/core/Avatar'
import auth from './../auth/auth-helper'
import {Link} from 'react-router-dom'
import Divider from '@material-ui/core/Divider'
import DeleteMedia from './DeleteMedia'
import MediaPlayer from './MediaPlayer'
const useStyles = makeStyles(theme => ({
card: {
padding:'20px'
},
header: {
padding:'0px 16px 16px 12px'
},
action: {
margin: '24px 20px 0px 0px',
display: 'inline-block',
fontSize: '1.15em',
color: theme.palette.secondary.dark
},
avatar: {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.light
}
}))
export default function Media(props) {
const classes = useStyles()
const mediaUrl = props.media._id
? `/api/media/video/${props.media._id}`
: null
const nextUrl = props.nextUrl
return (
<Card className={classes.card}>
<CardHeader className={classes.header}
title={props.media.title}
action={
<span className={classes.action}>{props.media.views + ' views'}</span>
}
subheader={props.media.genre}
/>
<MediaPlayer srcUrl={mediaUrl} nextUrl={nextUrl} handleAutoplay={props.handleAutoplay}/>
<List dense>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.avatar}>
{props.media.postedBy.name && props.media.postedBy.name[0]}
</Avatar>
</ListItemAvatar>
<ListItemText primary={props.media.postedBy.name}
secondary={"Published on " + (new Date(props.media.created)).toDateString()}/>
{ auth.isAuthenticated().user
&& auth.isAuthenticated().user._id == props.media.postedBy._id
&& (<ListItemSecondaryAction>
<Link to={"/media/edit/" + props.media._id}>
<IconButton aria-label="Edit" color="secondary">
<Edit/>
</IconButton>
</Link>
<DeleteMedia mediaId={props.media._id} mediaTitle={props.media.title}/>
</ListItemSecondaryAction>)
}
</ListItem>
<Divider/>
<ListItem>
<ListItemText primary={props.media.description}/>
</ListItem>
</List>
</Card>)
}
Media.propTypes = {
media: PropTypes.object,
nextUrl: PropTypes.string,
handleAutoplay: PropTypes.func.isRequired
}
================================================
FILE: client/media/MediaList.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import GridList from '@material-ui/core/GridList'
import GridListTileBar from '@material-ui/core/GridListTileBar'
import GridListTile from '@material-ui/core/GridListTile'
import {Link} from 'react-router-dom'
import ReactPlayer from 'react-player'
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
overflow: 'hidden',
background: theme.palette.background.paper,
textAlign: 'left',
padding: '8px 16px'
},
gridList: {
width: '100%',
minHeight: 180,
padding: '0px 0 10px'
},
title: {
padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(2)}px`,
color: theme.palette.openTitle,
width: '100%'
},
tile: {
textAlign: 'center',
maxHeight: '100%'
},
tileBar: {
backgroundColor: 'rgba(0, 0, 0, 0.72)',
textAlign: 'left',
height: '55px'
},
tileTitle: {
fontSize:'1.1em',
marginBottom:'5px',
color:'rgb(193, 173, 144)',
display:"block"
},
tileGenre: {
float: 'right',
color:'rgb(193, 182, 164)',
marginRight: '8px'
}
}))
export default function MediaList(props) {
const classes = useStyles()
return (
<div className={classes.root}>
<GridList className={classes.gridList} cols={3}>
{props.media.map((tile, i) => (
<GridListTile key={i} className={classes.tile}>
<Link to={"/media/"+tile._id}>
<ReactPlayer url={'/api/media/video/'+tile._id} width='100%' height='inherit' style={{maxHeight: '100%'}}/>
</Link>
<GridListTileBar className={classes.tileBar}
title={<Link to={"/media/"+tile._id} className={classes.tileTitle}> {tile.title} </Link>}
subtitle={<span>
<span>{tile.views} views</span>
<span className={classes.tileGenre}>
<em>{tile.genre}</em>
</span>
</span>}
/>
</GridListTile>
))}
</GridList>
</div>)
}
MediaList.propTypes = {
media: PropTypes.array.isRequired
}
================================================
FILE: client/media/MediaPlayer.js
================================================
import React, {useState, useEffect, useRef} from 'react'
import { findDOMNode } from 'react-dom'
import screenfull from 'screenfull'
import IconButton from '@material-ui/core/IconButton'
import Icon from '@material-ui/core/Icon'
import PropTypes from 'prop-types'
import {makeStyles} from '@material-ui/core/styles'
import { Link } from 'react-router-dom'
import ReactPlayer from 'react-player'
import LinearProgress from '@material-ui/core/LinearProgress'
const useStyles = makeStyles(theme => ({
flex:{
display:'flex'
},
primaryDashed: {
background: 'none',
backgroundColor: theme.palette.secondary.main
},
primaryColor: {
backgroundColor: '#6969694f'
},
dashed: {
animation: 'none'
},
controls:{
position: 'relative',
backgroundColor: '#ababab52'
},
rangeRoot: {
position: 'absolute',
width: '100%',
top: '-7px',
zIndex: '3456',
'-webkit-appearance': 'none',
backgroundColor: 'rgba(0,0,0,0)'
},
videoError: {
width: '100%',
textAlign: 'center',
color: theme.palette.primary.light
}
}))
export default function MediaPlayer(props) {
const classes = useStyles()
const [playing, setPlaying] = useState(false)
const [volume, setVolume] = useState(0.8)
const [muted, setMuted] = useState(false)
const [duration, setDuration] = useState(0)
const [seeking, setSeeking] = useState(false)
const [playbackRate, setPlaybackRate] = useState(1.0)
const [loop, setLoop] = useState(false)
const [fullscreen, setFullscreen] = useState(false)
const [videoError, setVideoError] = useState(false)
let playerRef = useRef(null)
const [values, setValues] = useState({
played: 0, loaded: 0, ended: false
})
useEffect(() => {
if (screenfull.enabled) {
screenfull.on('change', () => {
let fullscreen = screenfull.isFullscreen ? true : false
setFullscreen(fullscreen)
})
}
}, [])
useEffect(() => {
setVideoError(false)
}, [props.srcUrl])
const changeVolume = e => {
setVolume(parseFloat(e.target.value))
}
const toggleMuted = () => {
setMuted(!muted)
}
const playPause = () => {
setPlaying(!playing)
}
const onLoop = () => {
setLoop(!loop)
}
const onProgress = progress => {
// We only want to update time slider if we are not currently seeking
if (!seeking) {
setValues({...values, played:progress.played, loaded: progress.loaded})
}
}
const onClickFullscreen = () => {
screenfull.request(findDOMNode(playerRef))
}
const onEnded = () => {
if(loop){
setPlaying(true)
} else{
props.handleAutoplay(()=>{
setValues({...values, ended:true})
setPlaying(false)
})
}
}
const onDuration = (duration) => {
setDuration(duration)
}
const onSeekMouseDown = e => {
setSeeking(true)
}
const onSeekChange = e => {
setValues({...values, played:parseFloat(e.target.value), ended: parseFloat(e.target.value) >= 1})
}
const onSeekMouseUp = e => {
setSeeking(false)
playerRef.seekTo(parseFloat(e.target.value))
}
const ref = player => {
playerRef = player
}
const format = (seconds) => {
const date = new Date(seconds * 1000)
const hh = date.getUTCHours()
let mm = date.getUTCMinutes()
const ss = ('0' + date.getUTCSeconds()).slice(-2)
if (hh) {
mm = ('0' + date.getUTCMinutes()).slice(-2)
return `${hh}:${mm}:${ss}`
}
return `${mm}:${ss}`
}
const showVideoError = e => {
console.log(e)
setVideoError(true)
}
return (<div>
{videoError && <p className={classes.videoError}>Video Error. Try again later.</p>}
<div className={classes.flex}>
<ReactPlayer
ref={ref}
width={fullscreen ? '100%':'inherit'}
height={fullscreen ? '100%':'inherit'}
style={fullscreen ? {position:'relative'} : {maxHeight: '500px'}}
config={{ attributes: { style: { height: '100%', width: '100%'} } }}
url={props.srcUrl}
playing={playing}
loop={loop}
playbackRate={playbackRate}
volume={volume}
muted={muted}
onEnded={onEnded}
onError={showVideoError}
onProgress={onProgress}
onDuration={onDuration}/>
<br/>
</div>
<div className={classes.controls}>
<LinearProgress color="primary" variant="buffer" value={values.played*100} valueBuffer={values.loaded*100} style={{width: '100%'}} classes={{
colorPrimary: classes.primaryColor,
dashedColorPrimary : classes.primaryDashed,
dashed: classes.dashed
}}/>
<input type="range" min={0} max={1}
value={values.played} step='any'
onMouseDown={onSeekMouseDown}
onChange={onSeekChange}
onMouseUp={onSeekMouseUp}
className={classes.rangeRoot}/>
<IconButton color="primary" onClick={playPause}>
<Icon>{playing ? 'pause': (values.ended ? 'replay' : 'play_arrow')}</Icon>
</IconButton>
<IconButton disabled={!props.nextUrl} color="primary">
<Link to={props.nextUrl} style={{color: 'inherit'}}>
<Icon>skip_next</Icon>
</Link>
</IconButton>
<IconButton color="primary" onClick={toggleMuted}>
<Icon>{volume > 0 && !muted && 'volume_up' || muted && 'volume_off' || volume==0 && 'volume_mute'}</Icon>
</IconButton>
<input type="range" min={0} max={1} step='any' value={muted? 0 : volume} onChange={changeVolume} style={{verticalAlign: 'middle'}}/>
<IconButton color={loop? 'primary' : 'default'} onClick={onLoop}>
<Icon>loop</Icon>
</IconButton>
<IconButton color="primary" onClick={onClickFullscreen}>
<Icon>fullscreen</Icon>
</IconButton>
<span style={{float: 'right', padding: '10px', color: '#b83423'}}>
<time dateTime={`P${Math.round(duration * values.played)}S`}>
{format(duration * values.played)}
</time> / <time dateTime={`P${Math.round(duration)}S`}>
{format(duration)}
</time>
</span>
</div>
</div>
)
}
MediaPlayer.propTypes = {
srcUrl: PropTypes.string,
nextUrl: PropTypes.string,
handleAutoplay: PropTypes.func.isRequired
}
================================================
FILE: client/media/NewMedia.js
================================================
import React, {useState} from 'react'
import auth from './../auth/auth-helper'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import FileUpload from '@material-ui/icons/AddToQueue'
import Icon from '@material-ui/core/Icon'
import {makeStyles} from '@material-ui/core/styles'
import {create} from './api-media.js'
import {Redirect} from 'react-router-dom'
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 500,
margin: 'auto',
textAlign: 'center',
marginTop: theme.spacing(5),
paddingBottom: theme.spacing(2)
},
title: {
margin: theme.spacing(2),
color: theme.palette.protectedTitle,
fontSize: '1em'
},
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing(2)
},
input: {
display: 'none'
},
filename:{
marginLeft:'10px'
}
}))
export default function NewMedia(){
const classes = useStyles()
const [values, setValues] = useState({
title: '',
video: '',
description: '',
genre: '',
redirect: false,
error: '',
mediaId: ''
})
const jwt = auth.isAuthenticated()
const clickSubmit = () => {
let mediaData = new FormData()
values.title && mediaData.append('title', values.title)
values.video && mediaData.append('video', values.video)
values.description && mediaData.append('description', values.description)
values.genre && mediaData.append('genre', values.genre)
create({
userId: jwt.user._id
}, {
t: jwt.token
}, mediaData).then((data) => {
if (data.error) {
setValues({...values, error: data.error})
} else {
setValues({...values, error: '', mediaId: data._id, redirect: true})
}
})
}
const handleChange = name => event => {
const value = name === 'video'
? event.target.files[0]
: event.target.value
setValues({...values, [name]: value })
}
if (values.redirect) {
return (<Redirect to={'/media/' + values.mediaId}/>)
}
return (
<Card className={classes.card}>
<CardContent>
<Typography type="headline" component="h1" className={classes.title}>
New Video
</Typography>
<input accept="video/*" onChange={handleChange('video')} className={classes.input} id="icon-button-file" type="file" />
<label htmlFor="icon-button-file">
<Button color="secondary" variant="contained" component="span">
Upload
<FileUpload/>
</Button>
</label> <span className={classes.filename}>{values.video ? values.video.name : ''}</span><br/>
<TextField id="title" label="Title" className={classes.textField} value={values.title} onChange={handleChange('title')} margin="normal"/><br/>
<TextField
id="multiline-flexible"
label="Description"
multiline
rows="2"
value={values.description}
onChange={handleChange('description')}
className={classes.textField}
margin="normal"
/><br/>
<TextField id="genre" label="Genre" className={classes.textField} value={values.genre} onChange={handleChange('genre')} margin="normal"/><br/>
<br/> {
values.error && (<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{values.error}
</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit} className={classes.submit}>Submit</Button>
</CardActions>
</Card>
)
}
================================================
FILE: client/media/PlayMedia.js
================================================
import React, {useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import {makeStyles} from '@material-ui/core/styles'
import Grid from '@material-ui/core/Grid'
import {read, listRelated} from './api-media.js'
import Media from './Media'
import RelatedMedia from './RelatedMedia'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import Switch from '@material-ui/core/Switch'
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
margin: 30,
},
toggle: {
float: 'right',
marginRight: '30px',
marginTop:' 10px'
}
}))
export default function PlayMedia(props) {
const classes = useStyles()
let [media, setMedia] = useState({postedBy: {}})
let [relatedMedia, setRelatedMedia] = useState([])
const [autoPlay, setAutoPlay] = useState(false)
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
read({mediaId: props.match.params.mediaId}, signal).then((data) => {
if (data && data.error) {
console.log(data.error)
} else {
setMedia(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [props.match.params.mediaId])
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
listRelated({
mediaId: props.match.params.mediaId}, signal).then((data) => {
if (data.error) {
console.log(data.error)
} else {
setRelatedMedia(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [props.match.params.mediaId])
const handleChange = (event) => {
setAutoPlay(event.target.checked)
}
const handleAutoplay = (updateMediaControls) => {
let playList = relatedMedia
let playMedia = playList[0]
if(!autoPlay || playList.length == 0 )
return updateMediaControls()
if(playList.length > 1){
playList.shift()
setMedia(playMedia)
setRelatedMedia(playList)
}else{
listRelated({
mediaId: playMedia._id}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
setMedia(playMedia)
setRelatedMedia(data)
}
})
}
}
//render SSR data
if (props.data && props.data[0] != null) {
media = props.data[0]
relatedMedia = []
}
const nextUrl = relatedMedia.length > 0
? `/media/${relatedMedia[0]._id}` : ''
return (
<div className={classes.root}>
<Grid container spacing={8}>
<Grid item xs={8} sm={8}>
<Media media={media} nextUrl={nextUrl} handleAutoplay={handleAutoplay}/>
</Grid>
{relatedMedia.length > 0
&& (<Grid item xs={4} sm={4}>
<FormControlLabel className = {classes.toggle}
control={
<Switch
checked={autoPlay}
onChange={handleChange}
color="primary"
/>
}
label={autoPlay ? 'Autoplay ON':'Autoplay OFF'}
/>
<RelatedMedia media={relatedMedia}/>
</Grid>)
}
</Grid>
</div>)
}
================================================
FILE: client/media/RelatedMedia.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import {makeStyles} from '@material-ui/core/styles'
import Paper from '@material-ui/core/Paper'
import Typography from '@material-ui/core/Typography'
import {Link} from 'react-router-dom'
import Divider from '@material-ui/core/Divider'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import ReactPlayer from 'react-player'
const useStyles = makeStyles(theme => ({
root: theme.mixins.gutters({
paddingBottom: 24,
backgroundColor: '#80808024'
}),
title: {
margin: `${theme.spacing(3)}px ${theme.spacing(1)}px ${theme.spacing(2)}px`,
color: theme.palette.openTitle,
fontSize: '1em'
},
card: {
width: '100%',
display: 'inline-flex'
},
details: {
display: 'inline-block',
width: "100%"
},
content: {
flex: '1 0 auto',
padding: '16px 8px 0px'
},
controls: {
marginTop: '8px'
},
date: {
color: 'rgba(0, 0, 0, 0.4)'
},
mediaTitle: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '130px',
fontSize: '1em',
marginBottom: '5px'
},
subheading: {
color: 'rgba(88, 114, 128, 0.67)'
},
views: {
display: 'inline',
lineHeight: '3',
paddingLeft: '8px',
color: theme.palette.text.secondary
}
}))
export default function RelatedMedia(props) {
const classes = useStyles()
return (
<Paper className={classes.root} elevation={4} style={{padding: '16px'}}>
<Typography type="title" className={classes.title}>
Up Next
</Typography>
{props.media.map((item, i) => {
return <span key={i}><Card className={classes.card} >
<div style={{marginRight: "5px", backgroundColor: "black"}}>
<Link to={"/media/"+item._id}><ReactPlayer url={'/api/media/video/'+item._id} width='160px' height='140px'/></Link>
</div>
<div className={classes.details}>
<CardContent className={classes.content}>
<Link to={'/media/'+item._id}><Typography type="title" component="h3" className={classes.mediaTitle} color="primary">{item.title}</Typography></Link>
<Typography type="subheading" className={classes.subheading}>
{item.genre}
</Typography>
<Typography component="p" className={classes.date}>
{(new Date(item.created)).toDateString()}
</Typography>
</CardContent>
<div className={classes.controls}>
<Typography type="subheading" component="h3" className={classes.views} color="primary"> {item.views} views</Typography>
</div>
</div>
</Card>
<Divider/>
</span>
})
}
</Paper>
)
}
RelatedMedia.propTypes = {
media: PropTypes.array.isRequired
}
================================================
FILE: client/media/api-media.js
================================================
import config from '../../config/config'
const create = async (params, credentials, media) => {
try {
let response = await fetch('/api/media/new/'+ params.userId, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: media
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const listPopular = async (signal) => {
try {
let response = await fetch('/api/media/popular', {
method: 'GET',
signal: signal,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const listByUser = async (params) => {
try {
let response = await fetch('/api/media/by/'+ params.userId, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const read = async (params, signal) => {
try {
let response = await fetch(config.serverUrl +'/api/media/' + params.mediaId, {
method: 'GET',
signal: signal
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const update = async (params, credentials, media) => {
try {
let response = await fetch('/api/media/' + params.mediaId, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify(media)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const remove = async (params, credentials) => {
try {
let response = await fetch('/api/media/' + params.mediaId, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const listRelated = async (params, signal) => {
try {
let response = await fetch('/api/media/related/'+ params.mediaId, {
method: 'GET',
signal: signal,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
export {
create,
listPopular,
listByUser,
read,
update,
remove,
listRelated
}
================================================
FILE: client/routeConfig.js
================================================
import PlayMedia from './media/PlayMedia'
import { read } from './media/api-media.js'
const routes = [
{
path: '/media/:mediaId',
component: PlayMedia,
loadData: (params) => read(params)
}
]
export default routes
================================================
FILE: client/theme.js
================================================
import { createMuiTheme } from '@material-ui/core/styles'
import { red, brown } from '@material-ui/core/colors'
const theme = createMuiTheme({
typography: {
useNextVariants: true,
},
palette: {
primary: {
light: '#f05545',
main: '#b71c1c',
dark: '#7f0000',
contrastText: '#fff',
},
secondary: {
light: '#fbfffc',
main: '#c8e6c9',
dark: '#97b498',
contrastText: '#37474f',
},
openTitle: red['500'],
protectedTitle: brown['300'],
type: 'light'
},
})
export default theme
================================================
FILE: client/user/DeleteUser.js
================================================
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import IconButton from '@material-ui/core/IconButton'
import Button from '@material-ui/core/Button'
import DeleteIcon from '@material-ui/icons/Delete'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import auth from './../auth/auth-helper'
import {remove} from './api-user.js'
import {Redirect} from 'react-router-dom'
export default function DeleteUser(props) {
const [open, setOpen] = useState(false)
const [redirect, setRedirect] = useState(false)
const jwt = auth.isAuthenticated()
const clickButton = () => {
setOpen(true)
}
const deleteAccount = () => {
remove({
userId: props.userId
}, {t: jwt.token}).then((data) => {
if (data && data.error) {
console.log(data.error)
} else {
auth.clearJWT(() => console.log('deleted'))
setRedirect(true)
}
})
}
const handleRequestClose = () => {
setOpen(false)
}
if (redirect) {
return <Redirect to='/'/>
}
return (<span>
<IconButton aria-label="Delete" onClick={clickButton} color="secondary">
<DeleteIcon/>
</IconButton>
<Dialog open={open} onClose={handleRequestClose}>
<DialogTitle>{"Delete Account"}</DialogTitle>
<DialogContent>
<DialogContentText>
Confirm to delete your account.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleRequestClose} color="primary">
Cancel
</Button>
<Button onClick={deleteAccount} color="secondary" autoFocus="autoFocus">
Confirm
</Button>
</DialogActions>
</Dialog>
</span>)
}
DeleteUser.propTypes = {
userId: PropTypes.string.isRequired
}
================================================
FILE: client/user/EditProfile.js
================================================
import React, {useState, useEffect} from 'react'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import Icon from '@material-ui/core/Icon'
import { makeStyles } from '@material-ui/core/styles'
import auth from './../auth/auth-helper'
import {read, update} from './api-user.js'
import {Redirect} from 'react-router-dom'
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
margin: 'auto',
textAlign: 'center',
marginTop: theme.spacing(5),
paddingBottom: theme.spacing(2)
},
title: {
margin: theme.spacing(2),
color: theme.palette.protectedTitle
},
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing(2)
}
}))
export default function EditProfile({ match }) {
const classes = useStyles()
const [values, setValues] = useState({
name: '',
password: '',
email: '',
open: false,
error: '',
redirectToProfile: false
})
const jwt = auth.isAuthenticated()
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
read({
userId: match.params.userId
}, {t: jwt.token}, signal).then((data) => {
if (data && data.error) {
setValues({...values, error: data.error})
} else {
setValues({...values, name: data.name, email: data.email})
}
})
return function cleanup(){
abortController.abort()
}
}, [match.params.userId])
const clickSubmit = () => {
const user = {
name: values.name || undefined,
email: values.email || undefined,
password: values.password || undefined
}
update({
userId: match.params.userId
}, {
t: jwt.token
}, user).then((data) => {
if (data && data.error) {
setValues({...values, error: data.error})
} else {
setValues({...values, userId: data._id, redirectToProfile: true})
}
})
}
const handleChange = name => event => {
setValues({...values, [name]: event.target.value})
}
if (values.redirectToProfile) {
return (<Redirect to={'/user/' + values.userId}/>)
}
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="h6" className={classes.title}>
Edit Profile
</Typography>
<TextField id="name" label="Name" className={classes.textField} value={values.name} onChange={handleChange('name')} margin="normal"/><br/>
<TextField id="email" type="email" label="Email" className={classes.textField} value={values.email} onChange={handleChange('email')} margin="normal"/><br/>
<TextField id="password" type="password" label="Password" className={classes.textField} value={values.password} onChange={handleChange('password')} margin="normal"/>
<br/> {
values.error && (<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{values.error}
</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit} className={classes.submit}>Submit</Button>
</CardActions>
</Card>
)
}
================================================
FILE: client/user/Profile.js
================================================
import React, { useState, useEffect } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Paper from '@material-ui/core/Paper'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemAvatar from '@material-ui/core/ListItemAvatar'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Avatar from '@material-ui/core/Avatar'
import IconButton from '@material-ui/core/IconButton'
import Typography from '@material-ui/core/Typography'
import Edit from '@material-ui/icons/Edit'
import Person from '@material-ui/icons/Person'
import Divider from '@material-ui/core/Divider'
import DeleteUser from './DeleteUser'
import auth from './../auth/auth-helper'
import {read} from './api-user.js'
import {Redirect, Link} from 'react-router-dom'
import {listByUser} from '../media/api-media.js'
import MediaList from '../media/MediaList'
const useStyles = makeStyles(theme => ({
root: theme.mixins.gutters({
maxWidth: 600,
margin: 'auto',
padding: theme.spacing(3),
marginTop: theme.spacing(5)
}),
title: {
marginTop: theme.spacing(3),
color: theme.palette.protectedTitle
},
avatar: {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.light
}
}))
export default function Profile({ match }) {
const classes = useStyles()
const [user, setUser] = useState({})
const [redirectToSignin, setRedirectToSignin] = useState(false)
const jwt = auth.isAuthenticated()
const [media, setMedia] = useState([])
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
read({
userId: match.params.userId
}, {t: jwt.token}, signal).then((data) => {
if (data && data.error) {
setRedirectToSignin(true)
} else {
setUser(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [match.params.userId])
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
listByUser({
userId: match.params.userId
}, {t: jwt.token}, signal).then((data) => {
if (data && data.error) {
setRedirectToSignin(true)
} else {
setMedia(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [match.params.userId])
if (redirectToSignin) {
return <Redirect to='/signin'/>
}
return (
<Paper className={classes.root} elevation={4}>
<Typography variant="h6" className={classes.title}>
Profile
</Typography>
<List dense>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.avatar}>
{user.name && user.name[0]}
</Avatar>
</ListItemAvatar>
<ListItemText primary={user.name} secondary={user.email}/> {
auth.isAuthenticated().user && auth.isAuthenticated().user._id == user._id &&
(<ListItemSecondaryAction>
<Link to={"/user/edit/" + user._id}>
<IconButton aria-label="Edit" color="primary">
<Edit/>
</IconButton>
</Link>
<DeleteUser userId={user._id}/>
</ListItemSecondaryAction>)
}
</ListItem>
<Divider/>
<ListItem>
<ListItemText primary={"Joined: " + (
new Date(user.created)).toDateString()}/>
</ListItem>
<MediaList media={media}/>
</List>
</Paper>
)
}
================================================
FILE: client/user/Signup.js
================================================
import React, {useState} from 'react'
import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import Icon from '@material-ui/core/Icon'
import { makeStyles } from '@material-ui/core/styles'
import {create} from './api-user.js'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import {Link} from 'react-router-dom'
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
margin: 'auto',
textAlign: 'center',
marginTop: theme.spacing(5),
paddingBottom: theme.spacing(2)
},
error: {
verticalAlign: 'middle'
},
title: {
marginTop: theme.spacing(2),
color: theme.palette.openTitle
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing(2)
}
}))
export default function Signup() {
const classes = useStyles()
const [values, setValues] = useState({
name: '',
password: '',
email: '',
open: false,
error: ''
})
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value })
}
const clickSubmit = () => {
const user = {
name: values.name || undefined,
email: values.email || undefined,
password: values.password || undefined
}
create(user).then((data) => {
if (data.error) {
setValues({ ...values, error: data.error})
} else {
setValues({ ...values, error: '', open: true})
}
})
}
return (<div>
<Card className={classes.card}>
<CardContent>
<Typography variant="h6" className={classes.title}>
Sign Up
</Typography>
<TextField id="name" label="Name" className={classes.textField} value={values.name} onChange={handleChange('name')} margin="normal"/><br/>
<TextField id="email" type="email" label="Email" className={classes.textField} value={values.email} onChange={handleChange('email')} margin="normal"/><br/>
<TextField id="password" type="password" label="Password" className={classes.textField} value={values.password} onChange={handleChange('password')} margin="normal"/>
<br/> {
values.error && (<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{values.error}</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit} className={classes.submit}>Submit</Button>
</CardActions>
</Card>
<Dialog open={values.open} disableBackdropClick={true}>
<DialogTitle>New Account</DialogTitle>
<DialogContent>
<DialogContentText>
New account successfully created.
</DialogContentText>
</DialogContent>
<DialogActions>
<Link to="/signin">
<Button color="primary" autoFocus="autoFocus" variant="contained">
Sign In
</Button>
</Link>
</DialogActions>
</Dialog>
</div>
)
}
================================================
FILE: client/user/Users.js
================================================
import React, {useState, useEffect} from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Paper from '@material-ui/core/Paper'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemAvatar from '@material-ui/core/ListItemAvatar'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Avatar from '@material-ui/core/Avatar'
import IconButton from '@material-ui/core/IconButton'
import Typography from '@material-ui/core/Typography'
import ArrowForward from '@material-ui/icons/ArrowForward'
import Person from '@material-ui/icons/Person'
import {Link} from 'react-router-dom'
import {list} from './api-user.js'
const useStyles = makeStyles(theme => ({
root: theme.mixins.gutters({
padding: theme.spacing(1),
margin: theme.spacing(5)
}),
title: {
margin: `${theme.spacing(4)}px 0 ${theme.spacing(2)}px`,
color: theme.palette.openTitle
}
}))
export default function Users() {
const classes = useStyles()
const [users, setUsers] = useState([])
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
list(signal).then((data) => {
if (data && data.error) {
console.log(data.error)
} else {
setUsers(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [])
return (
<Paper className={classes.root} elevation={4}>
<Typography variant="h6" className={classes.title}>
All Users
</Typography>
<List dense>
{users.map((item, i) => {
return <Link to={"/user/" + item._id} key={i}>
<ListItem button>
<ListItemAvatar>
<Avatar>
<Person/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={item.name}/>
<ListItemSecondaryAction>
<IconButton>
<ArrowForward/>
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Link>
})
}
</List>
</Paper>
)
}
================================================
FILE: client/user/api-user.js
================================================
const create = async (user) => {
try {
let response = await fetch('/api/users/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const list = async (signal) => {
try {
let response = await fetch('/api/users/', {
method: 'GET',
signal: signal,
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const read = async (params, credentials, signal) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'GET',
signal: signal,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const update = async (params, credentials, user) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
const remove = async (params, credentials) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
export {
create,
list,
read,
update,
remove
}
================================================
FILE: config/config.js
================================================
const config = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3000,
jwtSecret: process.env.JWT_SECRET || "YOUR_secret_key",
mongoUri: process.env.MONGODB_URI ||
process.env.MONGO_HOST ||
'mongodb://' + (process.env.IP || 'localhost') + ':' +
(process.env.MONGO_PORT || '27017') +
'/mernproject',
serverUrl: process.env.serverUrl || 'http://localhost:3000'
}
export default config
================================================
FILE: nodemon.json
================================================
{
"verbose": false,
"watch": [
"./server"
],
"exec": "webpack --mode=development --config webpack.config.server.js && node ./dist/server.generated.js"
}
================================================
FILE: package.json
================================================
{
"name": "mern-mediastream",
"version": "2.0.0",
"description": "A MERN stack based media streaming application",
"author": "Shama Hoque",
"license": "MIT",
"keywords": [
"react",
"express",
"mongodb",
"node",
"mern"
],
"repository": {
"type": "git",
"url": "https://github.com/shamahoque/mern-mediastream.git"
},
"homepage": "https://github.com/shamahoque/mern-mediastream",
"main": "./dist/server.generated.js",
"scripts": {
"development": "nodemon",
"build": "webpack --config webpack.config.client.production.js && webpack --mode=production --config webpack.config.server.js",
"start": "NODE_ENV=production node ./dist/server.generated.js"
},
"engines": {
"node": "13.12.0",
"npm": "6.14.4"
},
"devDependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4",
"babel-loader": "8.1.0",
"file-loader": "6.0.0",
"nodemon": "2.0.2",
"webpack": "4.42.1",
"webpack-cli": "3.3.11",
"webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.0",
"webpack-node-externals": "1.7.2"
},
"dependencies": {
"@hot-loader/react-dom": "16.13.0",
"@material-ui/core": "4.9.8",
"@material-ui/icons": "4.9.1",
"body-parser": "1.19.0",
"compression": "1.7.4",
"cookie-parser": "1.4.5",
"cors": "2.8.5",
"express": "4.17.1",
"express-jwt": "5.3.1",
"formidable": "1.2.2",
"helmet": "3.22.0",
"isomorphic-fetch": "2.2.1",
"jsonwebtoken": "8.5.1",
"lodash": "4.17.15",
"mongoose": "5.9.7",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-hot-loader": "4.12.20",
"react-player": "1.15.3",
"react-router": "5.1.2",
"react-router-config": "5.1.1",
"react-router-dom": "5.1.2",
"screenfull": "5.0.2"
}
}
================================================
FILE: server/controllers/auth.controller.js
================================================
import User from '../models/user.model'
import jwt from 'jsonwebtoken'
import expressJwt from 'express-jwt'
import config from './../../config/config'
const signin = async (req, res) => {
try {
let user = await User.findOne({
"email": req.body.email
})
if (!user)
return res.status('401').json({
error: "User not found"
})
if (!user.authenticate(req.body.password)) {
return res.status('401').send({
error: "Email and password don't match."
})
}
const token = jwt.sign({
_id: user._id
}, config.jwtSecret)
res.cookie("t", token, {
expire: new Date() + 9999
})
return res.json({
token,
user: {
_id: user._id,
name: user.name,
email: user.email
}
})
} catch (err) {
return res.status('401').json({
error: "Could not sign in"
})
}
}
const signout = (req, res) => {
res.clearCookie("t")
return res.status('200').json({
message: "signed out"
})
}
const requireSignin = expressJwt({
secret: config.jwtSecret,
userProperty: 'auth'
})
const hasAuthorization = (req, res, next) => {
const authorized = req.profile && req.auth && req.profile._id == req.auth._id
if (!(authorized)) {
return res.status('403').json({
error: "User is not authorized"
})
}
next()
}
export default {
signin,
signout,
requireSignin,
hasAuthorization
}
================================================
FILE: server/controllers/media.controller.js
================================================
import Media from '../models/media.model'
import extend from 'lodash/extend'
import errorHandler from './../helpers/dbErrorHandler'
import formidable from 'formidable'
import fs from 'fs'
//media streaming
import mongoose from 'mongoose'
let gridfs = null
mongoose.connection.on('connected', () => {
gridfs = new mongoose.mongo.GridFSBucket(mongoose.connection.db)
})
const create = (req, res) => {
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Video could not be uploaded"
})
}
let media = new Media(fields)
media.postedBy= req.profile
if(files.video){
let writestream = gridfs.openUploadStream(media._id, {
contentType: files.video.type || 'binary/octet-stream'})
fs.createReadStream(files.video.path).pipe(writestream)
}
try {
let result = await media.save()
res.status(200).json(result)
}
catch (err){
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
})
}
const mediaByID = async (req, res, next, id) => {
try{
let media = await Media.findById(id).populate('postedBy', '_id name').exec()
if (!media)
return res.status('400').json({
error: "Media not found"
})
req.media = media
let files = await gridfs.find({filename:media._id}).toArray()
if (!files[0]) {
return res.status(404).send({
error: 'No video found'
})
}
req.file = files[0]
next()
}catch(err) {
return res.status(404).send({
error: 'Could not retrieve media file'
})
}
}
const video = (req, res) => {
const range = req.headers["range"]
if (range && typeof range === "string") {
const parts = range.replace(/bytes=/, "").split("-")
const partialstart = parts[0]
const partialend = parts[1]
const start = parseInt(partialstart, 10)
const end = partialend ? parseInt(partialend, 10) : req.file.length - 1
const chunksize = (end - start) + 1
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Range': 'bytes ' + start + '-' + end + '/' + req.file.length,
'Content-Type': req.file.contentType
})
let downloadStream = gridfs.openDownloadStream(req.file._id, {start, end: end+1})
downloadStream.pipe(res)
downloadStream.on('error', () => {
res.sendStatus(404)
})
downloadStream.on('end', () => {
res.end()
})
} else {
res.header('Content-Length', req.file.length)
res.header('Content-Type', req.file.contentType)
let downloadStream = gridfs.openDownloadStream(req.file._id)
downloadStream.pipe(res)
downloadStream.on('error', () => {
res.sendStatus(404)
})
downloadStream.on('end', () => {
res.end()
})
}
}
const listPopular = async (req, res) => {
try{
let media = await Media.find({}).limit(9)
.populate('postedBy', '_id name')
.sort('-views')
.exec()
res.json(media)
} catch(err){
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const listByUser = async (req, res) => {
try{
let media = await Media.find({postedBy: req.profile._id})
.populate('postedBy', '_id name')
.sort('-created')
.exec()
res.json(media)
} catch(err){
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const read = (req, res) => {
return res.json(req.media)
}
const incrementViews = async (req, res, next) => {
try {
await Media.findByIdAndUpdate(req.media._id, {$inc: {"views": 1}}, {new: true}).exec()
next()
} catch(err){
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const update = async (req, res) => {
try {
let media = req.media
media = extend(media, req.body)
media.updated = Date.now()
await media.save()
res.json(media)
} catch(err){
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const isPoster = (req, res, next) => {
let isPoster = req.media && req.auth && req.media.postedBy._id == req.auth._id
if(!isPoster){
return res.status('403').json({
error: "User is not authorized"
})
}
next()
}
const remove = async (req, res) => {
try {
let media = req.media
let deletedMedia = await media.remove()
gridfs.delete(req.file._id)
res.json(deletedMedia)
} catch(err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const listRelated = async (req, res) => {
try {
let media = await Media.find({ "_id": { "$ne": req.media }, "genre": req.media.genre})
.limit(4)
.sort('-views')
.populate('postedBy', '_id name')
.exec()
res.json(media)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
export default {
create,
mediaByID,
video,
listPopular,
listByUser,
read,
incrementViews,
update,
isPoster,
remove,
listRelated
}
================================================
FILE: server/controllers/user.controller.js
================================================
import User from '../models/user.model'
import extend from 'lodash/extend'
import errorHandler from './../helpers/dbErrorHandler'
const create = async (req, res) => {
const user = new User(req.body)
try {
await user.save()
return res.status(200).json({
message: "Successfully signed up!"
})
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
/**
* Load user and append to req.
*/
const userByID = async (req, res, next, id) => {
try {
let user = await User.findById(id)
if (!user)
return res.status('400').json({
error: "User not found"
})
req.profile = user
next()
} catch (err) {
return res.status('400').json({
error: "Could not retrieve user"
})
}
}
const read = (req, res) => {
req.profile.hashed_password = undefined
req.profile.salt = undefined
return res.json(req.profile)
}
const list = async (req, res) => {
try {
let users = await User.find().select('name email updated created')
res.json(users)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const update = async (req, res) => {
try {
let user = req.profile
user = extend(user, req.body)
user.updated = Date.now()
await user.save()
user.hashed_password = undefined
user.salt = undefined
res.json(user)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
const remove = async (req, res) => {
try {
let user = req.profile
let deletedUser = await user.remove()
deletedUser.hashed_password = undefined
deletedUser.salt = undefined
res.json(deletedUser)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
export default {
create,
userByID,
read,
list,
remove,
update
}
================================================
FILE: server/devBundle.js
================================================
import config from './../config/config'
import webpack from 'webpack'
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import webpackConfig from './../webpack.config.client.js'
const compile = (app) => {
if(config.env === "development"){
const compiler = webpack(webpackConfig)
const middleware = webpackMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
})
app.use(middleware)
app.use(webpackHotMiddleware(compiler))
}
}
export default {
compile
}
================================================
FILE: server/express.js
================================================
import express from 'express'
import path from 'path'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import compress from 'compression'
import cors from 'cors'
import helmet from 'helmet'
import Template from './../template'
import userRoutes from './routes/user.routes'
import authRoutes from './routes/auth.routes'
import mediaRoutes from './routes/media.routes'
// modules for server side rendering
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import MainRouter from './../client/MainRouter'
import { StaticRouter } from 'react-router-dom'
import { ServerStyleSheets, ThemeProvider } from '@material-ui/styles'
import theme from './../client/theme'
//end
//For SSR with data
import { matchRoutes } from 'react-router-config'
import routes from './../client/routeConfig'
import 'isomorphic-fetch'
//end
//comment out before building for production
import devBundle from './devBundle'
const CURRENT_WORKING_DIR = process.cwd()
const app = express()
//comment out before building for production
devBundle.compile(app)
//For SSR with data
const loadBranchData = (location) => {
const branch = matchRoutes(routes, location)
const promises = branch.map(({ route, match }) => {
return route.loadData
? route.loadData(branch[0].match.params)
: Promise.resolve(null)
})
return Promise.all(promises)
}
// parse body params and attache them to req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser())
app.use(compress())
// secure apps by setting various HTTP headers
app.use(helmet())
// enable CORS - Cross Origin Resource Sharing
app.use(cors())
app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist')))
// mount routes
app.use('/', userRoutes)
app.use('/', authRoutes)
app.use('/', mediaRoutes)
app.get('*', (req, res) => {
const sheets = new ServerStyleSheets()
const context = {}
loadBranchData(req.url).then(data => {
const markup = ReactDOMServer.renderToString(
sheets.collect(
<StaticRouter location={req.url} context={context}>
<ThemeProvider theme={theme}>
<MainRouter data={data}/>
</ThemeProvider>
</StaticRouter>
)
)
if (context.url) {
return res.redirect(303, context.url)
}
const css = sheets.toString()
res.status(200).send(Template({
markup: markup,
css: css
}))
}).catch(err => {
res.status(500).send({"error": "Could not load React view with data"})
})
})
// Catch unauthorised errors
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).json({"error" : err.name + ": " + err.message})
}else if (err) {
res.status(400).json({"error" : err.name + ": " + err.message})
console.log(err)
}
})
export default app
================================================
FILE: server/helpers/dbErrorHandler.js
================================================
'use strict'
/**
* Get unique error field name
*/
const getUniqueErrorMessage = (err) => {
let output
try {
let fieldName = err.message.substring(err.message.lastIndexOf('.$') + 2, err.message.lastIndexOf('_1'))
output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists'
} catch (ex) {
output = 'Unique field already exists'
}
return output
}
/**
* Get the error message from error object
*/
const getErrorMessage = (err) => {
let message = ''
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = getUniqueErrorMessage(err)
break
default:
message = 'Something went wrong'
}
} else {
for (let errName in err.errors) {
if (err.errors[errName].message) message = err.errors[errName].message
}
}
return message
}
export default {getErrorMessage}
================================================
FILE: server/models/media.model.js
================================================
import mongoose from 'mongoose'
const MediaSchema = new mongoose.Schema({
title: {
type: String,
required: 'title is required'
},
description: String,
genre: String,
views: {type: Number, default: 0},
postedBy: {type: mongoose.Schema.ObjectId, ref: 'User'},
created: {
type: Date,
default: Date.now
},
updated: {
type: Date
}
})
export default mongoose.model('Media', MediaSchema)
================================================
FILE: server/models/user.model.js
================================================
import mongoose from 'mongoose'
import crypto from 'crypto'
const UserSchema = new mongoose.Schema({
name: {
type: String,
trim: true,
required: 'Name is required'
},
email: {
type: String,
trim: true,
unique: 'Email already exists',
match: [/.+\@.+\..+/, 'Please fill a valid email address'],
required: 'Email is required'
},
hashed_password: {
type: String,
required: "Password is required"
},
salt: String,
updated: Date,
created: {
type: Date,
default: Date.now
}
})
UserSchema
.virtual('password')
.set(function(password) {
this._password = password
this.salt = this.makeSalt()
this.hashed_password = this.encryptPassword(password)
})
.get(function() {
return this._password
})
UserSchema.path('hashed_password').validate(function(v) {
if (this._password && this._password.length < 6) {
this.invalidate('password', 'Password must be at least 6 characters.')
}
if (this.isNew && !this._password) {
this.invalidate('password', 'Password is required')
}
}, null)
UserSchema.methods = {
authenticate: function(plainText) {
return this.encryptPassword(plainText) === this.hashed_password
},
encryptPassword: function(password) {
if (!password) return ''
try {
return crypto
.createHmac('sha1', this.salt)
.update(password)
.digest('hex')
} catch (err) {
return ''
}
},
makeSalt: function() {
return Math.round((new Date().valueOf() * Math.random())) + ''
}
}
export default mongoose.model('User', UserSchema)
================================================
FILE: server/routes/auth.routes.js
================================================
import express from 'express'
import authCtrl from '../controllers/auth.controller'
const router = express.Router()
router.route('/auth/signin')
.post(authCtrl.signin)
router.route('/auth/signout')
.get(authCtrl.signout)
export default router
================================================
FILE: server/routes/media.routes.js
================================================
import express from 'express'
import userCtrl from '../controllers/user.controller'
import authCtrl from '../controllers/auth.controller'
import mediaCtrl from '../controllers/media.controller'
const router = express.Router()
router.route('/api/media/new/:userId')
.post(authCtrl.requireSignin, mediaCtrl.create)
router.route('/api/media/video/:mediaId')
.get(mediaCtrl.video)
router.route('/api/media/popular')
.get(mediaCtrl.listPopular)
router.route('/api/media/related/:mediaId')
.get(mediaCtrl.listRelated)
router.route('/api/media/by/:userId')
.get(mediaCtrl.listByUser)
router.route('/api/media/:mediaId')
.get( mediaCtrl.incrementViews, mediaCtrl.read)
.put(authCtrl.requireSignin, mediaCtrl.isPoster, mediaCtrl.update)
.delete(authCtrl.requireSignin, mediaCtrl.isPoster, mediaCtrl.remove)
router.param('userId', userCtrl.userByID)
router.param('mediaId', mediaCtrl.mediaByID)
export default router
================================================
FILE: server/routes/user.routes.js
================================================
import express from 'express'
import userCtrl from '../controllers/user.controller'
import authCtrl from '../controllers/auth.controller'
const router = express.Router()
router.route('/api/users')
.get(userCtrl.list)
.post(userCtrl.create)
router.route('/api/users/:userId')
.get(authCtrl.requireSignin, userCtrl.read)
.put(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.update)
.delete(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.remove)
router.param('userId', userCtrl.userByID)
export default router
================================================
FILE: server/server.js
================================================
import config from './../config/config'
import app from './express'
import mongoose from 'mongoose'
// Connection URL
mongoose.Promise = global.Promise
mongoose.connect(config.mongoUri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true, useFindAndModify: false })
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${config.mongoUri}`)
})
app.listen(config.port, (err) => {
if (err) {
console.log(err)
}
console.info('Server started on port %s.', config.port)
})
================================================
FILE: template.js
================================================
export default ({markup, css}) => {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MERN Mediastream</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
a{
text-decoration: none
}
</style>
</head>
<body style="margin:0">
<div id="root">${markup}</div>
<style id="jss-server-side">${css}</style>
<script type="text/javascript" src="/dist/bundle.js"></script>
</body>
</html>`
}
================================================
FILE: webpack.config.client.js
================================================
const path = require('path')
const webpack = require('webpack')
const CURRENT_WORKING_DIR = process.cwd()
const config = {
name: "browser",
mode: "development",
devtool: 'eval-source-map',
entry: [
'react-hot-loader/patch',
'webpack-hot-middleware/client?reload=true',
path.join(CURRENT_WORKING_DIR, 'client/main.js')
],
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist'),
filename: 'bundle.js',
publicPath: '/dist/'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
'babel-loader'
]
},
{
test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
use: 'file-loader'
}
]
}, plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
module.exports = config
================================================
FILE: webpack.config.client.production.js
================================================
const path = require('path')
const CURRENT_WORKING_DIR = process.cwd()
const config = {
mode: "production",
entry: [
path.join(CURRENT_WORKING_DIR, 'client/main.js')
],
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist'),
filename: 'bundle.js',
publicPath: "/dist/"
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
'babel-loader'
]
},
{
test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
use: 'file-loader'
}
]
}
}
module.exports = config
================================================
FILE: webpack.config.server.js
================================================
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const CURRENT_WORKING_DIR = process.cwd()
const config = {
name: "server",
entry: [ path.join(CURRENT_WORKING_DIR , './server/server.js') ],
target: "node",
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist/'),
filename: "server.generated.js",
publicPath: '/dist/',
libraryTarget: "commonjs2"
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [ 'babel-loader' ]
},
{
test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
use: 'file-loader'
}
]
}
}
module.exports = config
gitextract_hdvuq4ke/ ├── .babelrc ├── .github/ │ └── stale.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── client/ │ ├── App.js │ ├── MainRouter.js │ ├── auth/ │ │ ├── PrivateRoute.js │ │ ├── Signin.js │ │ ├── api-auth.js │ │ └── auth-helper.js │ ├── core/ │ │ ├── Home.js │ │ └── Menu.js │ ├── main.js │ ├── media/ │ │ ├── DeleteMedia.js │ │ ├── EditMedia.js │ │ ├── Media.js │ │ ├── MediaList.js │ │ ├── MediaPlayer.js │ │ ├── NewMedia.js │ │ ├── PlayMedia.js │ │ ├── RelatedMedia.js │ │ └── api-media.js │ ├── routeConfig.js │ ├── theme.js │ └── user/ │ ├── DeleteUser.js │ ├── EditProfile.js │ ├── Profile.js │ ├── Signup.js │ ├── Users.js │ └── api-user.js ├── config/ │ └── config.js ├── nodemon.json ├── package.json ├── server/ │ ├── controllers/ │ │ ├── auth.controller.js │ │ ├── media.controller.js │ │ └── user.controller.js │ ├── devBundle.js │ ├── express.js │ ├── helpers/ │ │ └── dbErrorHandler.js │ ├── models/ │ │ ├── media.model.js │ │ └── user.model.js │ ├── routes/ │ │ ├── auth.routes.js │ │ ├── media.routes.js │ │ └── user.routes.js │ └── server.js ├── template.js ├── webpack.config.client.js ├── webpack.config.client.production.js └── webpack.config.server.js
SYMBOL INDEX (22 symbols across 20 files)
FILE: client/auth/Signin.js
function Signin (line 40) | function Signin(props) {
FILE: client/auth/auth-helper.js
method isAuthenticated (line 4) | isAuthenticated() {
method authenticate (line 13) | authenticate(jwt, cb) {
method clearJWT (line 18) | clearJWT(cb) {
FILE: client/core/Home.js
function Home (line 22) | function Home(){
FILE: client/media/DeleteMedia.js
function DeleteMedia (line 15) | function DeleteMedia(props) {
FILE: client/media/EditMedia.js
function EditProfile (line 47) | function EditProfile({ match }) {
FILE: client/media/Media.js
function Media (line 39) | function Media(props) {
FILE: client/media/MediaList.js
function MediaList (line 52) | function MediaList(props) {
FILE: client/media/MediaPlayer.js
function MediaPlayer (line 45) | function MediaPlayer(props) {
FILE: client/media/NewMedia.js
function NewMedia (line 48) | function NewMedia(){
FILE: client/media/PlayMedia.js
function PlayMedia (line 23) | function PlayMedia(props) {
FILE: client/media/RelatedMedia.js
function RelatedMedia (line 58) | function RelatedMedia(props) {
FILE: client/user/DeleteUser.js
function DeleteUser (line 15) | function DeleteUser(props) {
FILE: client/user/EditProfile.js
function EditProfile (line 40) | function EditProfile({ match }) {
FILE: client/user/Profile.js
function Profile (line 39) | function Profile({ match }) {
FILE: client/user/Signup.js
function Signup (line 44) | function Signup() {
FILE: client/user/Users.js
function Users (line 28) | function Users() {
FILE: server/express.js
constant CURRENT_WORKING_DIR (line 32) | const CURRENT_WORKING_DIR = process.cwd()
FILE: webpack.config.client.js
constant CURRENT_WORKING_DIR (line 3) | const CURRENT_WORKING_DIR = process.cwd()
FILE: webpack.config.client.production.js
constant CURRENT_WORKING_DIR (line 2) | const CURRENT_WORKING_DIR = process.cwd()
FILE: webpack.config.server.js
constant CURRENT_WORKING_DIR (line 3) | const CURRENT_WORKING_DIR = process.cwd()
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (94K chars).
[
{
"path": ".babelrc",
"chars": 206,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\",\n {\n \"targets\": {\n \"node\": \"current\"\n }\n }\n "
},
{
"path": ".github/stale.yml",
"chars": 685,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
},
{
"path": ".gitignore",
"chars": 43,
"preview": "/node_modules/\n/dist/\n/data/\nnpm-debug.log\n"
},
{
"path": "LICENSE.md",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2018 Shama Hoque\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 3824,
"preview": "# MERN Mediastream 2.0\n- *Looking for the first edition code? [Check here](https://github.com/shamahoque/mern-mediastrea"
},
{
"path": "client/App.js",
"chars": 593,
"preview": "import React from 'react'\nimport MainRouter from './MainRouter'\nimport {BrowserRouter} from 'react-router-dom'\nimport { "
},
{
"path": "client/MainRouter.js",
"chars": 1232,
"preview": "import React, {Component} from 'react'\nimport {Route, Switch} from 'react-router-dom'\nimport Home from './core/Home'\nimp"
},
{
"path": "client/auth/PrivateRoute.js",
"chars": 443,
"preview": "import React, { Component } from 'react'\nimport { Route, Redirect } from 'react-router-dom'\nimport auth from './auth-hel"
},
{
"path": "client/auth/Signin.js",
"chars": 2927,
"preview": "import React, {useState} from 'react'\nimport Card from '@material-ui/core/Card'\nimport CardActions from '@material-ui/co"
},
{
"path": "client/auth/api-auth.js",
"chars": 583,
"preview": "const signin = async (user) => {\n try {\n let response = await fetch('/auth/signin/', {\n method: 'POST',\n h"
},
{
"path": "client/auth/auth-helper.js",
"chars": 659,
"preview": "import { signout } from './api-auth.js'\n\nconst auth = {\n isAuthenticated() {\n if (typeof window == \"undefined\")\n "
},
{
"path": "client/core/Home.js",
"chars": 1226,
"preview": "import React, {useState, useEffect} from 'react'\nimport { makeStyles } from '@material-ui/core/styles'\nimport Card from "
},
{
"path": "client/core/Menu.js",
"chars": 2088,
"preview": "import React from 'react'\nimport AppBar from '@material-ui/core/AppBar'\nimport Toolbar from '@material-ui/core/Toolbar'\n"
},
{
"path": "client/main.js",
"chars": 136,
"preview": "import React from 'react'\nimport { hydrate } from 'react-dom'\nimport App from './App'\n\nhydrate(<App/>, document.getEleme"
},
{
"path": "client/media/DeleteMedia.js",
"chars": 2075,
"preview": "import React, {useState} from 'react'\nimport PropTypes from 'prop-types'\nimport IconButton from '@material-ui/core/IconB"
},
{
"path": "client/media/EditMedia.js",
"chars": 3630,
"preview": "import React, {useState, useEffect} from 'react'\nimport Card from '@material-ui/core/Card'\nimport CardActions from '@mat"
},
{
"path": "client/media/Media.js",
"chars": 3073,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport { makeStyles } from '@material-ui/core/styles'\nimpor"
},
{
"path": "client/media/MediaList.js",
"chars": 2276,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport { makeStyles } from '@material-ui/core/styles'\nimpor"
},
{
"path": "client/media/MediaPlayer.js",
"chars": 6458,
"preview": "import React, {useState, useEffect, useRef} from 'react'\nimport { findDOMNode } from 'react-dom'\nimport screenfull from "
},
{
"path": "client/media/NewMedia.js",
"chars": 4101,
"preview": "import React, {useState} from 'react'\nimport auth from './../auth/auth-helper'\nimport Card from '@material-ui/core/Card'"
},
{
"path": "client/media/PlayMedia.js",
"chars": 3388,
"preview": "import React, {useState, useEffect} from 'react'\nimport PropTypes from 'prop-types'\nimport {makeStyles} from '@material-"
},
{
"path": "client/media/RelatedMedia.js",
"chars": 3131,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport {makeStyles} from '@material-ui/core/styles'\nimport "
},
{
"path": "client/media/api-media.js",
"chars": 2533,
"preview": "import config from '../../config/config'\nconst create = async (params, credentials, media) => {\n try {\n let response"
},
{
"path": "client/routeConfig.js",
"chars": 231,
"preview": "import PlayMedia from './media/PlayMedia'\nimport { read } from './media/api-media.js'\n\nconst routes = [\n {\n path: '/"
},
{
"path": "client/theme.js",
"chars": 612,
"preview": "import { createMuiTheme } from '@material-ui/core/styles'\nimport { red, brown } from '@material-ui/core/colors'\n\nconst t"
},
{
"path": "client/user/DeleteUser.js",
"chars": 2041,
"preview": "import React, {useState} from 'react'\nimport PropTypes from 'prop-types'\nimport IconButton from '@material-ui/core/IconB"
},
{
"path": "client/user/EditProfile.js",
"chars": 3616,
"preview": "import React, {useState, useEffect} from 'react'\nimport Card from '@material-ui/core/Card'\nimport CardActions from '@mat"
},
{
"path": "client/user/Profile.js",
"chars": 3723,
"preview": "import React, { useState, useEffect } from 'react'\nimport { makeStyles } from '@material-ui/core/styles'\nimport Paper fr"
},
{
"path": "client/user/Signup.js",
"chars": 3585,
"preview": "import React, {useState} from 'react'\nimport Card from '@material-ui/core/Card'\nimport CardActions from '@material-ui/co"
},
{
"path": "client/user/Users.js",
"chars": 2375,
"preview": "import React, {useState, useEffect} from 'react'\nimport { makeStyles } from '@material-ui/core/styles'\nimport Paper from"
},
{
"path": "client/user/api-user.js",
"chars": 1847,
"preview": "const create = async (user) => {\n try {\n let response = await fetch('/api/users/', {\n method: 'POST',\n "
},
{
"path": "config/config.js",
"chars": 432,
"preview": "const config = {\n env: process.env.NODE_ENV || 'development',\n port: process.env.PORT || 3000,\n jwtSecret: process.en"
},
{
"path": "nodemon.json",
"chars": 175,
"preview": "{\n \"verbose\": false,\n \"watch\": [\n \"./server\"\n ],\n \"exec\": \"webpack --mode=development --config webpack."
},
{
"path": "package.json",
"chars": 1855,
"preview": "{\n \"name\": \"mern-mediastream\",\n \"version\": \"2.0.0\",\n \"description\": \"A MERN stack based media streaming application\","
},
{
"path": "server/controllers/auth.controller.js",
"chars": 1439,
"preview": "import User from '../models/user.model'\nimport jwt from 'jsonwebtoken'\nimport expressJwt from 'express-jwt'\nimport confi"
},
{
"path": "server/controllers/media.controller.js",
"chars": 5330,
"preview": "import Media from '../models/media.model'\nimport extend from 'lodash/extend'\nimport errorHandler from './../helpers/dbEr"
},
{
"path": "server/controllers/user.controller.js",
"chars": 1939,
"preview": "import User from '../models/user.model'\nimport extend from 'lodash/extend'\nimport errorHandler from './../helpers/dbErro"
},
{
"path": "server/devBundle.js",
"chars": 563,
"preview": "import config from './../config/config'\nimport webpack from 'webpack'\nimport webpackMiddleware from 'webpack-dev-middlew"
},
{
"path": "server/express.js",
"chars": 2915,
"preview": "import express from 'express'\nimport path from 'path'\nimport bodyParser from 'body-parser'\nimport cookieParser from 'coo"
},
{
"path": "server/helpers/dbErrorHandler.js",
"chars": 986,
"preview": "'use strict'\n\n/**\n * Get unique error field name\n */\nconst getUniqueErrorMessage = (err) => {\n let output\n try {\n "
},
{
"path": "server/models/media.model.js",
"chars": 422,
"preview": "import mongoose from 'mongoose'\nconst MediaSchema = new mongoose.Schema({\n title: {\n type: String,\n required: 'ti"
},
{
"path": "server/models/user.model.js",
"chars": 1595,
"preview": "import mongoose from 'mongoose'\nimport crypto from 'crypto'\nconst UserSchema = new mongoose.Schema({\n name: {\n type:"
},
{
"path": "server/routes/auth.routes.js",
"chars": 250,
"preview": "import express from 'express'\nimport authCtrl from '../controllers/auth.controller'\n\nconst router = express.Router()\n\nro"
},
{
"path": "server/routes/media.routes.js",
"chars": 948,
"preview": "import express from 'express'\nimport userCtrl from '../controllers/user.controller'\nimport authCtrl from '../controllers"
},
{
"path": "server/routes/user.routes.js",
"chars": 547,
"preview": "import express from 'express'\nimport userCtrl from '../controllers/user.controller'\nimport authCtrl from '../controllers"
},
{
"path": "server/server.js",
"chars": 536,
"preview": "import config from './../config/config'\nimport app from './express'\nimport mongoose from 'mongoose'\n\n// Connection URL\nm"
},
{
"path": "template.js",
"chars": 730,
"preview": "export default ({markup, css}) => {\n return `<!doctype html>\n <html lang=\"en\">\n <head>\n <meta ch"
},
{
"path": "webpack.config.client.js",
"chars": 1003,
"preview": "const path = require('path')\nconst webpack = require('webpack')\nconst CURRENT_WORKING_DIR = process.cwd()\n\nconst config "
},
{
"path": "webpack.config.client.production.js",
"chars": 709,
"preview": "const path = require('path')\nconst CURRENT_WORKING_DIR = process.cwd()\n\nconst config = {\n mode: \"production\",\n ent"
},
{
"path": "webpack.config.server.js",
"chars": 817,
"preview": "const path = require('path')\nconst nodeExternals = require('webpack-node-externals')\nconst CURRENT_WORKING_DIR = process"
}
]
About this extraction
This page contains the full source code of the shamahoque/mern-mediastream GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (85.5 KB), approximately 22.4k tokens, and a symbol index with 22 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.