Skip to content

Commit

Permalink
Implement a basic password-protected admin page
Browse files Browse the repository at this point in the history
  • Loading branch information
Slava committed Mar 28, 2019
1 parent b18d812 commit 2924fc5
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 16 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -72,6 +72,7 @@ The following environment variables can be tweaked:
- `API_PORT` - to differentiate the port for the API to run on (should be only used in dev)
- `UPLOADS_PATH` - absolute path where the app stores uploaded images, defaults to server's folder 'uploads'
- `DATABASE_FILE_PATH` - absolute path of the file where the app stores the SQLite data. Defaults to `database.sqlite` in the server folder
- `ADMIN_PASSWORD` - sets a simple password on all non-labeler actions (stored in a hased form).

## Run in Docker

Expand Down Expand Up @@ -109,6 +110,7 @@ CURRENT_UID=$(id -u):$(id -g) docker-compose up -d --build
# if it only needs to run,
CURRENT_UID=$(id -u):$(id -g) docker-compose up -d
```

## Project Support and Development

This project has been developed as part of my internship at the [NCSOFT](http://global.ncsoft.com/global/) Vision AI Lab in the beginning of 2019.
3 changes: 1 addition & 2 deletions client/src/App.js
@@ -1,5 +1,5 @@
import React, { Component, Fragment } from 'react';
import { Route, Redirect, BrowserRouter as Router } from 'react-router-dom';
import { Route, BrowserRouter as Router } from 'react-router-dom';

import LabelHome from './label/LabelHome';
import LabelingLoader from './label/LabelingLoader';
Expand All @@ -21,7 +21,6 @@ class App extends Component {
replace: () => {},
push: () => {},
goBack: () => {},
replace: () => {},
},
};
return <LabelingLoader {...props} />;
Expand Down
2 changes: 2 additions & 0 deletions client/src/admin/AdminApp.js
Expand Up @@ -5,6 +5,7 @@ import './AdminApp.css';
import Menubar from '../common/Menubar';
import ProjectsGrid from '../common/ProjectsGrid';
import ProjectPage from './ProjectPage';
import LoginPage from './LoginPage';

class AdminApp extends Component {
render() {
Expand All @@ -22,6 +23,7 @@ class AdminApp extends Component {
/>
)}
/>
<Route exact path="/admin/login" component={LoginPage} />
<Route path="/admin/:projectId" component={ProjectPage} />
</Switch>
</Menubar>
Expand Down
42 changes: 42 additions & 0 deletions client/src/admin/LoginPage.js
@@ -0,0 +1,42 @@
import React, { Component } from 'react';

import { Header, Form, Segment } from 'semantic-ui-react';

export default class LoginPage extends Component {
constructor(props) {
super(props);
this.state = {
password: '',
error: null,
};
this.onSubmit = this.onSubmit.bind(this);
}

async onSubmit(e) {
const { password } = this.state;
const resp = await fetch('/api/auth?password=' + password);
if (resp.ok) {
this.setState({ error: null });
window.location = '/admin/';
} else {
this.setState({ error: 'Wrong password' });
}
}

render() {
return (
<Segment>
<Form onSubmit={this.onSubmit}>
<Header>Admin Login</Header>
<Form.Input
onChange={(e, { value }) => this.setState({ password: value })}
type="password"
label="Password"
/>
<Form.Button>Submit</Form.Button>
<p style={{ color: 'red' }}>{this.state.error}</p>
</Form>
</Segment>
);
}
}
7 changes: 6 additions & 1 deletion client/src/common/ProjectsGrid.js
Expand Up @@ -16,7 +16,12 @@ export default class ProjectsGrid extends Component {

async componentDidMount() {
try {
const projects = await (await fetch('/api/projects/')).json();
const r = await fetch('/api/projects/');
if (!r.ok && r.status === 401) {
window.location = '/admin/login/';
return;
}
const projects = await r.json();
this.setState({
isLoaded: true,
projects,
Expand Down
3 changes: 3 additions & 0 deletions server/package.json
Expand Up @@ -6,10 +6,13 @@
"private": true,
"dependencies": {
"archiver": "^3.0.0",
"bcrypt": "^3.0.5",
"better-sqlite3": "^5.4.0",
"body-parser": "^1.18.3",
"cookie-parser": "^1.4.4",
"eslint-plugin-prettier": "^3.0.1",
"express": "^4.16.4",
"express-session": "^1.15.6",
"husky": "^1.3.1",
"multer": "^1.4.1",
"nodemon": "^1.18.9",
Expand Down
46 changes: 46 additions & 0 deletions server/src/auth.js
@@ -0,0 +1,46 @@
const bcrypt = require('bcrypt');
const cookieParser = require('cookie-parser');
var session = require('express-session');

const pw = process.env.ADMIN_PASSWORD;
const hash = pw ? bcrypt.hashSync(pw, bcrypt.genSaltSync()) : null;

exports.setup = app => {
app.use(cookieParser());
app.use(
session({
key: 'user_sid',
secret: 'secretsecret33939',
resave: false,
saveUninitialized: false,
cookie: {
expires: 600000,
},
})
);

app.use((req, res, next) => {
if (req.cookies.user_sid && !req.session.user) {
res.clearCookie('user_sid');
}
next();
});
};

exports.checkLoginMiddleware = (req, res, next) => {
if (req.session.user && req.cookies.user_sid) {
next();
} else {
res.status(401).send({ message: 'unauthenticated' });
}
};

exports.authHandler = (req, res, next) => {
const { password } = req.query;
if (!hash || bcrypt.compareSync(password, hash)) {
req.session.user = true;
res.json({ success: true });
} else {
res.status(401).send({ message: 'unauthenticated' });
}
};
35 changes: 23 additions & 12 deletions server/src/index.js
Expand Up @@ -12,20 +12,22 @@ const images = require('./queries/images');
const mlmodels = require('./queries/mlmodels');
const exporter = require('./exporter');
const importer = require('./importer');
const { setup, checkLoginMiddleware, authHandler } = require('./auth');

const UPLOADS_PATH =
process.env.UPLOADS_PATH || path.join(__dirname, '..', 'uploads');

const app = express();

setup(app);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json({ limit: '10mb' }));

app.get('/api/mlmodels', (req, res) => {
res.json(mlmodels.getAll());
});

app.post('/api/mlmodels', (req, res) => {
app.post('/api/mlmodels', checkLoginMiddleware, (req, res) => {
// TODO: sanitize input data
const { model } = req.body;
const id = mlmodels.create(model);
Expand All @@ -49,25 +51,25 @@ app.post('/api/mlmodels/:id', (req, res) => {
.pipe(res);
});

app.delete('/api/mlmodels/:id', (req, res) => {
app.delete('/api/mlmodels/:id', checkLoginMiddleware, (req, res) => {
const { id } = req.params;
const model = mlmodels.delete(id);
res.json({ success: true });
});

app.get('/api/projects', (req, res) => {
app.get('/api/projects', checkLoginMiddleware, (req, res) => {
res.json(projects.getAll());
});

app.post('/api/projects', (req, res) => {
app.post('/api/projects', checkLoginMiddleware, (req, res) => {
res.json(projects.create());
});

app.get('/api/projects/:id', (req, res) => {
res.json(projects.get(req.params.id));
});

app.patch('/api/projects/:id', (req, res) => {
app.patch('/api/projects/:id', checkLoginMiddleware, (req, res) => {
const { project } = req.body;
try {
projects.update(req.params.id, project);
Expand All @@ -83,7 +85,7 @@ app.patch('/api/projects/:id', (req, res) => {
res.json({ success: true });
});

app.delete('/api/projects/:id', (req, res) => {
app.delete('/api/projects/:id', checkLoginMiddleware, (req, res) => {
projects.delete(req.params.id);
res.json({ success: true });
});
Expand All @@ -96,7 +98,7 @@ app.get('/api/images/:id', (req, res) => {
res.json(images.get(req.params.id));
});

app.post('/api/images', async (req, res) => {
app.post('/api/images', checkLoginMiddleware, async (req, res) => {
const { projectId, urls, localPath } = req.body;
if (urls) {
try {
Expand Down Expand Up @@ -146,7 +148,7 @@ app.post('/api/images', async (req, res) => {
}
});

app.delete('/api/images/:id', (req, res) => {
app.delete('/api/images/:id', checkLoginMiddleware, (req, res) => {
images.delete(req.params.id);
res.json({ success: true });
});
Expand Down Expand Up @@ -244,12 +246,18 @@ const uploads = multer({
}),
});

app.post('/api/uploads/:projectId', uploads.array('images'), (req, res) => {
res.json({ success: true });
});
app.post(
'/api/uploads/:projectId',
checkLoginMiddleware,
uploads.array('images'),
(req, res) => {
res.json({ success: true });
}
);

app.post(
'/api/uploads/:projectId/reference',
checkLoginMiddleware,
(req, res, next) => {
req.reference = true;
next();
Expand All @@ -265,6 +273,7 @@ const imports = multer({
});
app.post(
'/api/import/:projectId',
checkLoginMiddleware,
(req, res, next) => {
req.importRes = [];
next();
Expand Down Expand Up @@ -293,7 +302,7 @@ app.get('/uploads/:projectId/:imageName', (req, res) => {
res.sendFile(path.join(UPLOADS_PATH, projectId, path.join('/', imageName)));
});

app.get('/api/projects/:projectId/export', (req, res) => {
app.get('/api/projects/:projectId/export', checkLoginMiddleware, (req, res) => {
const archive = archiver('zip');

archive.on('error', err => {
Expand All @@ -312,6 +321,8 @@ app.get('/api/projects/:projectId/export', (req, res) => {
archive.finalize();
});

app.get('/api/auth', authHandler);

if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../../client/build')));
app.get('*', (req, res, next) => {
Expand Down

0 comments on commit 2924fc5

Please sign in to comment.