summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/client/auth/auth.actions.js82
-rw-r--r--app/client/auth/auth.gate.js41
-rw-r--r--app/client/auth/auth.reducer.js82
-rw-r--r--app/client/auth/index.js11
-rw-r--r--app/client/auth/login.component.js86
-rw-r--r--app/client/auth/logout.component.js24
-rw-r--r--app/client/auth/signup.component.js101
-rw-r--r--app/client/common/index.js3
-rw-r--r--app/client/common/textInput.component.js3
-rw-r--r--app/client/index.jsx27
-rw-r--r--app/client/store.js2
-rw-r--r--app/client/types.js7
-rw-r--r--app/server/site.js5
-rw-r--r--app/server/util/auth.js153
14 files changed, 613 insertions, 14 deletions
diff --git a/app/client/auth/auth.actions.js b/app/client/auth/auth.actions.js
new file mode 100644
index 0000000..5968f87
--- /dev/null
+++ b/app/client/auth/auth.actions.js
@@ -0,0 +1,82 @@
+import * as types from '../types';
+
+export const setToken = (data) => {
+ return { type: types.auth.set_token, data }
+}
+export const setError = (data) => {
+ return { type: types.auth.set_error, data }
+}
+export const setCurrentUser = (data) => {
+ return { type: types.auth.set_current_user, data }
+}
+export function logout() {
+ return { type: types.auth.logout_user };
+}
+export function authLoading() {
+ return { type: types.auth.loading };
+}
+
+export function InvalidCredentialsException(message) {
+ this.message = message;
+ this.name = 'InvalidCredentialsException';
+}
+
+export function login(username, password) {
+ return (dispatch) => {
+ dispatch(authLoading());
+ apiClient()
+ .post(api.GET_TOKEN, {
+ username,
+ password
+ })
+ .then(function (response) {
+ dispatch(setToken(response.data.token));
+ dispatch(getCurrentUser());
+ })
+ .catch(function (error) {
+ dispatch(setError(true));
+ if (error.response.status === 400) {
+ throw new InvalidCredentialsException(error);
+ }
+ throw error;
+ });
+ };
+}
+
+export function signup(data) {
+ return (dispatch) => {
+ dispatch(authLoading());
+ apiClient()
+ .post(api.SIGNUP, data)
+ .then(function (response) {
+ console.log(response.data);
+ dispatch(login(data.username, data.password));
+ })
+ .catch(function (error) {
+ console.log(error)
+ if (error.response.status === 400) {
+ // dispatch(accountError("There was an error creating your account."))
+ throw new InvalidCredentialsException(error);
+ }
+ throw error;
+ });
+ };
+}
+
+export function getCurrentUser() {
+ return (dispatch) => {
+ dispatch(authLoading());
+ apiClient()
+ .get(api.CURRENT_USER)
+ .then(function (response) {
+ dispatch(setCurrentUser(response.data));
+ console.log('set current user')
+ })
+ .catch(function (error) {
+ if (error.response.status === 400) {
+ throw new InvalidCredentialsException(error);
+ }
+ throw error;
+ });
+ };
+}
diff --git a/app/client/auth/auth.gate.js b/app/client/auth/auth.gate.js
new file mode 100644
index 0000000..e7a9940
--- /dev/null
+++ b/app/client/auth/auth.gate.js
@@ -0,0 +1,41 @@
+import { h, Component } from 'preact';
+// import PropTypes from 'prop-types';
+import { BrowserRouter, Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+
+import Login from './login.component';
+import Logout from './logout.component';
+import Signup from './signup.component';
+
+import { randint } from '../util/math'
+
+class AuthGate extends Component {
+ render(){
+ if (this.props.auth.isAuthenticated) return children
+ return (
+ <BrowserRouter>
+ <div>
+ <div className="spinfx"></div>
+ <Route exact path='/' component={Login} />
+ <Route exact path='/login' component={Login} />
+ <Route exact path='/logout' component={Logout} />
+ <Route exact path='/signup' component={Signup} />
+ </div>
+ </BrowserRouter>
+ )
+ }
+ componentDidMount(){
+ document.querySelector('.spinfx').style.backgroundImage = 'linear-gradient(' + (randint(40)-5) + 'deg, #fde, #ffe)'
+ }
+}
+
+const mapStateToProps = (state) => ({
+ auth: state.auth
+});
+
+const mapDispatchToProps = (dispatch) => ({
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(AuthGate);
diff --git a/app/client/auth/auth.reducer.js b/app/client/auth/auth.reducer.js
new file mode 100644
index 0000000..cacb0d5
--- /dev/null
+++ b/app/client/auth/auth.reducer.js
@@ -0,0 +1,82 @@
+import types from '../types';
+
+const authInitialState = {
+ token: null,
+ user: {},
+ groups: {},
+ loading: false,
+ isAuthenticated: false,
+};
+
+const auth = (state = authInitialState, action) => {
+ switch(action.type) {
+ case types.auth.set_token:
+ return {
+ ...state,
+ token: action.data,
+ isAuthenticated: !!action.data,
+ loading: false,
+ error: null,
+ };
+
+ case types.auth.loading:
+ return {
+ ...state,
+ loading: true,
+ error: null,
+ };
+
+ case types.auth.set_current_user:
+ const groups = {}
+ action.data.groups.forEach(g => groups[g.name.toLowerCase()] = true)
+ if (action.data.is_staff) {
+ groups['staff'] = true
+ }
+ if (action.data.is_superuser) {
+ groups['superuser'] = true
+ }
+ return {
+ ...state,
+ user: action.data,
+ groups,
+ error: null,
+ };
+
+ case types.auth.logout_user:
+ return {
+ ...authInitialState
+ };
+
+ case types.auth.set_error:
+ return {
+ ...state,
+ loading: false,
+ error: action.data,
+ }
+
+ case types.auth.loading:
+ // const initial_state_el = document.querySelector('#initial_state')
+ // if (initial_state_el) {
+ // try {
+ // const initial_state = JSON.parse(initial_state_el.innerHTML)
+ // if (initial_state && initial_state.auth && initial_state.auth.user) {
+ // console.log(initial_state.auth.user)
+ // return {
+ // ...state,
+ // user: {
+ // ...initial_state.auth.user,
+ // }
+ // }
+ // }
+ // } catch (e) {
+ // console.error("error loading initial state")
+ // }
+ // }
+ return state;
+
+ default:
+ return state;
+ }
+}
+
+export default auth;
diff --git a/app/client/auth/index.js b/app/client/auth/index.js
new file mode 100644
index 0000000..5e6b2b0
--- /dev/null
+++ b/app/client/auth/index.js
@@ -0,0 +1,11 @@
+import Gate from './auth.gate';
+import Login from './login.component';
+import Logout from './logout.component';
+import Signup from './signup.component';
+
+export default {
+ Gate,
+ Login,
+ Logout,
+ Signup,
+} \ No newline at end of file
diff --git a/app/client/auth/login.component.js b/app/client/auth/login.component.js
new file mode 100644
index 0000000..4ffab34
--- /dev/null
+++ b/app/client/auth/login.component.js
@@ -0,0 +1,86 @@
+import { h, Component } from 'preact';
+// import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+// import { Link } from 'react-router-dom';
+import * as authActions from './auth.actions';
+
+import { Group, Param, TextInput, Button } from '../common';
+
+class Login extends Component {
+ state = {
+ username: '',
+ password: '',
+ }
+ constructor() {
+ super()
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+ handleChange(e) {
+ const name = e.target.name
+ const value = e.target.value
+ this.setState({
+ [name]: value,
+ error: null,
+ })
+ }
+ handleSubmit(e) {
+ e.preventDefault()
+ this.props.actions.login(this.state.username, this.state.password)
+ }
+ render(){
+ if (this.props.auth.isAuthenticated) {
+ return <Redirect to="/" />
+ }
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <h1>Log in</h1><br />
+ <Group>
+ <TextInput
+ autofocus
+ autocapitalize="off"
+ autocomplete="off"
+ title="Username"
+ name="username"
+ type="text"
+ value={this.state.username}
+ onChange={this.handleChange}
+ />
+ <TextInput
+ title="Password"
+ name="password"
+ type="password"
+ value={this.state.password}
+ onChange={this.handleChange}
+ />
+ <Button
+ loading={this.props.auth.loading}
+ >
+ Login
+ </Button>
+ {this.renderAuthError()}
+ </Group>
+ </form>
+ )
+ }
+ renderAuthError(){
+ if (this.props.auth.error) {
+ return (
+ <div className='form-input-hint'>{"There was an error logging you in (bad password?)"}</div>
+ )
+ }
+ return null
+ }
+}
+
+const mapStateToProps = (state) => ({
+ auth: state.auth,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators(authActions, dispatch)
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Login);
diff --git a/app/client/auth/logout.component.js b/app/client/auth/logout.component.js
new file mode 100644
index 0000000..bcc3bce
--- /dev/null
+++ b/app/client/auth/logout.component.js
@@ -0,0 +1,24 @@
+import { h, Component } from 'preact';
+// import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+import * as authActions from './auth.actions';
+
+class Logout extends Component {
+ componentWillMount(props){
+ this.props.actions.logout()
+ }
+ render(){
+ return <Redirect to="/" />
+ }
+}
+
+const mapStateToProps = (state) => ({
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators(authActions, dispatch)
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Logout);
diff --git a/app/client/auth/signup.component.js b/app/client/auth/signup.component.js
new file mode 100644
index 0000000..c86d31b
--- /dev/null
+++ b/app/client/auth/signup.component.js
@@ -0,0 +1,101 @@
+import { h, Component } from 'preact';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+import actions from './auth.actions';
+
+import { Group, Param, TextInput, Button } from '../common';
+
+class Signup extends Component {
+ state = {
+ username: '',
+ password: '',
+ password2: '',
+ }
+ constructor() {
+ super()
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+ handleChange(e) {
+ const name = e.target.name
+ const value = e.target.value
+ this.setState({
+ [name]: value,
+ error: null,
+ })
+ }
+ validate(){
+ if (!this.state.password || this.state.password !== this.state.password2) {
+ return false
+ }
+ return true
+ }
+ handleSubmit(e) {
+ e.preventDefault()
+ if (!this.validate) {
+ return this.props.actions.setError('bad password')
+ }
+ this.props.actions.signup(this.state)
+ }
+ render(){
+ if (this.props.auth.isAuthenticated) {
+ return <Redirect to="/" />
+ }
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <h1>New account</h1><br />
+ <Group>
+ <TextInput
+ autofocus
+ autocapitalize="off"
+ autocomplete="off"
+ title="Username"
+ name="username"
+ type="text"
+ value={this.state.username}
+ onChange={this.handleChange}
+ />
+ <TextInput
+ title="Password"
+ name="password"
+ type="password"
+ value={this.state.password}
+ onChange={this.handleChange}
+ />
+ <TextInput
+ title="Password again :)"
+ name="password2"
+ type="password"
+ value={this.state.password2}
+ onChange={this.handleChange}
+ />
+ <Button
+ loading={this.props.auth.loading}
+ >
+ Login
+ </Button>
+ {this.renderAuthError()}
+ </Group>
+ </form>
+ )
+ }
+ renderAuthError(){
+ if (this.props.auth.error) {
+ return (
+ <div className='form-input-hint'>{"Please doublecheck the form"}</div>
+ )
+ }
+ return null
+ }
+}
+
+const mapStateToProps = (state) => ({
+ auth: state.auth,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators({ ...actions }, dispatch)
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Signup);
diff --git a/app/client/common/index.js b/app/client/common/index.js
index 7448104..e6baafc 100644
--- a/app/client/common/index.js
+++ b/app/client/common/index.js
@@ -1,3 +1,4 @@
+import AudioPlayer from './audioPlayer/audioPlayer.component'
import AugmentationGrid from './augmentationGrid.component'
import Button from './button.component'
import ButtonGrid from './buttonGrid.component'
@@ -24,7 +25,7 @@ import * as Views from './views'
export {
Views,
- Loading, Progress, Header,
+ Loading, Progress, Header, AudioPlayer,
FolderList, FileList, FileRow, FileUpload,
Gallery, Player,
Group, ParamGroup, Param,
diff --git a/app/client/common/textInput.component.js b/app/client/common/textInput.component.js
index d429944..d3b16ad 100644
--- a/app/client/common/textInput.component.js
+++ b/app/client/common/textInput.component.js
@@ -34,6 +34,9 @@ class TextInput extends Component {
value={this.state.changed ? this.state.value : this.props.value}
onInput={this.handleInput}
onKeydown={this.handleKeydown}
+ autofocus={this.props.autofocus}
+ autoComplete={this.props.autocomplete}
+ autoCapitalize={this.props.autocapitalize || 'off'}
placeholder={this.props.placeholder}
autofocus={this.props.autofocus}
className={this.props.className || ''}
diff --git a/app/client/index.jsx b/app/client/index.jsx
index fd4679c..614bb35 100644
--- a/app/client/index.jsx
+++ b/app/client/index.jsx
@@ -7,8 +7,8 @@ import { store, history } from './store'
import * as socket from './socket'
import util from './util'
-import Header from './common/header.component'
-import AudioPlayer from './common/audioPlayer/audioPlayer.component'
+import Auth from './auth'
+import { Header, AudioPlayer } from './common'
import System from './system/system.component'
import Dashboard from './dashboard/dashboard.component'
import modules from './modules'
@@ -22,16 +22,19 @@ const module_list = Object.keys(modules).map(name => {
const app = (
<Provider store={store}>
- <BrowserRouter>
- <div>
- <Route exact path='/' component={Dashboard} />
- <Route path='/system/' component={System} />
- <Route path='/dashboard/' component={Dashboard} />
- {module_list}
- <Route path='/' component={Header} />
- <AudioPlayer />
- </div>
- </BrowserRouter>
+ <Auth.Gate>
+ <BrowserRouter>
+ <div>
+ <Route exact path='/' component={Dashboard} />
+ <Route path='/system/' component={System} />
+ <Route path='/dashboard/' component={Dashboard} />
+ <Route path='/logout/' component={Auth.Logout} />
+ {module_list}
+ <Route path='/' component={Header} />
+ <AudioPlayer />
+ </div>
+ </BrowserRouter>
+ </Auth.Gate>
</Provider>
)
diff --git a/app/client/store.js b/app/client/store.js
index 8ffab15..654b22d 100644
--- a/app/client/store.js
+++ b/app/client/store.js
@@ -6,6 +6,7 @@ import createHistory from 'history/createBrowserHistory'
import { routerReducer } from 'react-router-redux'
// import navReducer from './nav.reducer'
+import authReducer from './auth/auth.reducer'
import systemReducer from './system/system.reducer'
import dashboardReducer from './dashboard/dashboard.reducer'
import liveReducer from './live/live.reducer'
@@ -15,6 +16,7 @@ import audioPlayerReducer from './common/audioPlayer/audioPlayer.reducer'
import { moduleReducer } from './modules/module.reducer'
const appReducer = combineReducers({
+ auth: authReducer,
system: systemReducer,
dashboard: dashboardReducer,
live: liveReducer,
diff --git a/app/client/types.js b/app/client/types.js
index 22211dd..44fe434 100644
--- a/app/client/types.js
+++ b/app/client/types.js
@@ -36,6 +36,13 @@ export default {
'progress',
'epoch',
]),
+ auth: crud_type('auth', [
+ 'set_token',
+ 'set_error',
+ 'set_current_user',
+ 'logout_user',
+ 'loading',
+ ]),
socket: {
connect: 'SOCKET_CONNECT',
connect_error: 'SOCKET_CONNECT_ERROR',
diff --git a/app/server/site.js b/app/server/site.js
index 85c932f..3c58862 100644
--- a/app/server/site.js
+++ b/app/server/site.js
@@ -2,12 +2,14 @@ const express = require('express')
const http = require('http')
const path = require('path')
const multer = require('multer')()
-const upload = require('./util/upload')
const bodyParser = require('body-parser')
const compression = require('compression')
// const multer = require('multer')
// const upload = multer({ dest: 'uploads/' })
+const upload = require('./util/upload')
+const auth = require('./util/auth')
+
export const app = new express()
export const server = http.createServer(app)
@@ -104,6 +106,7 @@ app.get('/:module/:mode/', serve_index)
app.get('/system/', serve_index)
app.get('/dashboard/', serve_index)
app.get('/', serve_index)
+auth.route(app, serve_index)
server.listen(process.env.EXPRESS_PORT, () => {
console.log('Cortex remote listening on http://localhost:' + server.address().port)
diff --git a/app/server/util/auth.js b/app/server/util/auth.js
new file mode 100644
index 0000000..d280927
--- /dev/null
+++ b/app/server/util/auth.js
@@ -0,0 +1,153 @@
+let passport = require('passport')
+let LocalStrategy = require('passport-local').Strategy
+let crypto = require('crypto')
+// let fs = require('fs')
+let db = require('../db')
+
+export function route(app, serve_index){
+ passport.serializeUser(serializeUser)
+ passport.deserializeUser(deserializeUser)
+ passport.use(new LocalStrategy(verifyLocalUser))
+
+ app.get("/login", serve_index)
+ app.get("/signup", serve_index)
+ app.get("/logout", logout)
+
+ app.put("/api/signup",
+ checkIfUserExists,
+ createUser,
+ passport.authenticate("local"),
+ login)
+ app.put("/api/login",
+ passport.authenticate("local"),
+ login)
+ app.put("/api/checkin",
+ ensureAuthenticated,
+ checkin
+ )
+}
+
+export function ensureAuthenticated(req, res, next) {
+ if (!req.isAuthenticated()) {
+ req.session.returnTo = req.path
+ return res.redirect('/login')
+ }
+ next()
+}
+
+export function checkIfUserExists(req, res, next) {
+ db.getUserByUsername(sanitizeName(req.body.username)).then((user) => {
+ user ? res.json({ error: "user exists" }) : next()
+ })
+}
+
+export function sanitizeName(s) { return (s || "").replace(new RegExp("[^-_a-zA-Z0-9]", 'g'), "") }
+export function sanitizeUser(req_user) {
+ // sanitize user object
+ var user = JSON.parse(JSON.stringify(req_user))
+ delete user.password
+ return user
+}
+
+export function createUser(req, res, next) {
+ if (req.body.password !== req.body.password2) {
+ return res.json({ error: "passwords don't match" })
+ }
+ let data = {
+ username: sanitizeName(req.body.username),
+ realname: sanitize(req.body.realname),
+ password: makePassword(username, req.body.password),
+ firstseen: new Date(),
+ lastseen: new Date(),
+ // lastsession: util.now(),
+ }
+ db.createUser(data).then(() => next())
+}
+
+export function login(req, res) {
+ if (req.isAuthenticated()) {
+ let returnTo = req.session.returnTo
+ delete req.session.returnTo
+ console.log(">> logged in", req.user.get('username'))
+ return res.json({
+ status: "OK",
+ user: sanitizeUser(req.user),
+ returnTo: returnTo || "/index",
+ })
+ }
+ res.json({
+ error: 'bad credentials',
+ })
+}
+
+export function serializeUser(user, done) {
+ done(null, user.id)
+}
+
+export function deserializeUser(id, done) {
+ db.getUser(id).then(function(user){
+ done(! user, user)
+ })
+}
+
+export function makePassword(password) {
+ let shasum = crypto.createHash('sha1')
+ shasum.update(password)
+ return shasum.digest('hex')
+}
+
+export function validPassword(user, password) {
+ return user.get('password') === makePassword(password)
+}
+
+export function changePassword(req, res, next) {
+ if (! req.body.oldpassword && ! req.body.newpassword) return next()
+ if (req.body.newpassword !== req.body.newpassword2) {
+ return res.send({ error: 'Passwords don\'t match.' })
+ }
+ if (! validPassword(res.user, req.body.oldpassword)) {
+ return res.send({ error: 'Password is incorrect.' })
+ }
+ let username = req.user.get('username')
+ let newPassword = makePassword(username, req.body.newpassword)
+ res.user.set('password', newPassword)
+ res.user.save().then(() => next()).catch(err => res.send({ error: err }))
+}
+export function changePasswordDangerously(req, res, next) {
+ if (! req.body.password && ! req.body.newpassword) return next()
+ if (req.body.newpassword !== req.body.newpassword2) {
+ return res.send({ error: 'Passwords don\'t match.' })
+ }
+ if (! validPassword(req.user, req.body.password)) {
+ return res.send({ error: 'Password is incorrect.' })
+ }
+ let username = res.user.get('username')
+ let newPassword = makePassword(username, req.body.newpassword)
+ res.user.set('password', newPassword)
+ res.user.save().then(() => next()).catch(err => res.send({ error: err }))
+}
+
+export function verifyLocalUser(username, password, done) {
+ // handle passwords!!
+ db.getUserByUsername(username).then(function(user){
+
+ // if (err) { return done(err) }
+ if (! user) { return done("no user") }
+
+ // return done(null, user)
+ if (! user || ! validPassword(user, password)) {
+ return done(null, false, { error: { message: 'Bad username/password.' } })
+ }
+ return done(null, user)
+ })
+}
+
+
+export function checkin(req, res) {
+ res.json({ user: sanitizeUser(req.user) })
+}
+
+export const logout = (req, res) => {
+ req.logout()
+ res.redirect('/')
+} \ No newline at end of file