Skip to content

Commit 04dc950

Browse files
committed
initial
0 parents  commit 04dc950

31 files changed

+1962
-0
lines changed

.eslintrc.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
module.exports = {
4+
rules: {
5+
indent: [2, 4, {
6+
SwitchCase: 1
7+
}],
8+
quotes: [2, 'single'],
9+
'linebreak-style': [2, 'unix'],
10+
semi: [2, 'always'],
11+
strict: [2, 'global'],
12+
eqeqeq: 2,
13+
'dot-notation': 2,
14+
curly: 2,
15+
'no-fallthrough': 2,
16+
'quote-props': [2, 'as-needed'],
17+
'no-unused-expressions': [2, {
18+
allowShortCircuit: true
19+
}],
20+
'no-unused-vars': 2,
21+
'no-undefined': 2,
22+
'handle-callback-err': 2,
23+
'no-new': 2,
24+
'new-cap': 2,
25+
'no-eval': 2,
26+
'no-invalid-this': 2,
27+
radix: [2, 'always'],
28+
'no-use-before-define': [2, 'nofunc'],
29+
'callback-return': [2, ['callback', 'cb', 'done']],
30+
'comma-dangle': [2, 'never'],
31+
'comma-style': [2, 'last'],
32+
'no-regex-spaces': 2,
33+
'no-empty': 2,
34+
'no-duplicate-case': 2,
35+
'no-empty-character-class': 2,
36+
'no-redeclare': [2, {
37+
builtinGlobals: true
38+
}],
39+
'block-scoped-var': 2,
40+
'no-sequences': 2,
41+
'no-throw-literal': 2,
42+
'no-useless-call': 2,
43+
'no-useless-concat': 2,
44+
'no-void': 2,
45+
yoda: 2,
46+
'no-undef': 2,
47+
'global-require': 2,
48+
'no-var': 2,
49+
'no-bitwise': 2,
50+
'no-lonely-if': 2,
51+
'no-mixed-spaces-and-tabs': 2,
52+
'arrow-body-style': [2, 'as-needed'],
53+
'arrow-parens': [2, 'as-needed'],
54+
'prefer-arrow-callback': 2,
55+
'object-shorthand': 2,
56+
'prefer-spread': 2
57+
},
58+
env: {
59+
es6: true,
60+
node: true
61+
},
62+
extends: 'eslint:recommended',
63+
globals: {
64+
it: true,
65+
describe: true,
66+
beforeEach: true,
67+
afterEach: true
68+
},
69+
fix: true
70+
};

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
.DS_Store
3+
npm-debug.log
4+
config/production.*
5+
config/development.*
6+
*v8.log

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# zmta-webadmin
2+
3+
Web administration component for [ZoneMTA](https://github.com/zone-eu/zone-mta). Adds a lightweight web based wrapper around ZoneMTA HTTP API calls.
4+
5+
## Setup
6+
7+
1. Copy these files to the same server where ZoneMTA runs.
8+
2. Install dependencies by running `npm install --production`
9+
3. Start the server with `npm start`
10+
4. Point your browser to http://localhost:8082
11+
12+
If you want to acces the administration page from outside current server then either modify configuration options or serve the page through Nginx or Apache.
13+
14+
## License

app.js

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
'use strict';
2+
3+
let config = require('config');
4+
let log = require('npmlog');
5+
6+
let express = require('express');
7+
let bodyParser = require('body-parser');
8+
let path = require('path');
9+
let favicon = require('serve-favicon');
10+
let logger = require('morgan');
11+
let cookieParser = require('cookie-parser');
12+
let session = require('express-session');
13+
let RedisStore = require('connect-redis')(session);
14+
let flash = require('connect-flash');
15+
let hbs = require('hbs');
16+
let compression = require('compression');
17+
let auth = require('basic-auth');
18+
let urllib = require('url');
19+
const humanize = require('humanize');
20+
21+
let routes = require('./routes/index');
22+
23+
let app = express();
24+
25+
// view engine setup
26+
app.set('views', path.join(__dirname, 'views'));
27+
app.set('view engine', 'hbs');
28+
29+
// Handle proxies. Needed to resolve client IP
30+
if (config.proxy) {
31+
app.set('trust proxy', config.proxy);
32+
}
33+
34+
// Do not expose software used
35+
app.disable('x-powered-by');
36+
37+
/**
38+
* We need this helper to make sure that we consume flash messages only
39+
* when we are able to actually display these. Otherwise we might end up
40+
* in a situation where we consume a flash messages but then comes a redirect
41+
* and the message is never displayed
42+
*/
43+
hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer-arrow-callback
44+
if (typeof this.flash !== 'function') { // eslint-disable-line no-invalid-this
45+
return '';
46+
}
47+
48+
let messages = this.flash(); // eslint-disable-line no-invalid-this
49+
let response = [];
50+
51+
// group messages by type
52+
Object.keys(messages).forEach(key => {
53+
let el = '<div class="alert alert-' + key + ' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
54+
55+
if (key === 'danger') {
56+
el += '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ';
57+
}
58+
59+
let rows = [];
60+
61+
messages[key].forEach(message => {
62+
rows.push(hbs.handlebars.escapeExpression(message));
63+
});
64+
65+
if (rows.length > 1) {
66+
el += '<p>' + rows.join('</p>\n<p>') + '</p>';
67+
} else {
68+
el += rows.join('');
69+
}
70+
71+
el += '</div>';
72+
73+
response.push(el);
74+
});
75+
76+
return new hbs.handlebars.SafeString(
77+
response.join('\n')
78+
);
79+
});
80+
81+
hbs.registerHelper('num', function (options) { // eslint-disable-line prefer-arrow-callback
82+
return new hbs.handlebars.SafeString(
83+
humanize.numberFormat(options.fn(this), 0, ',', ' ') // eslint-disable-line no-invalid-this
84+
);
85+
});
86+
87+
hbs.registerHelper('dec', function (options) { // eslint-disable-line prefer-arrow-callback
88+
return new hbs.handlebars.SafeString(
89+
humanize.numberFormat(options.fn(this), 3, ',', ' ') // eslint-disable-line no-invalid-this
90+
);
91+
});
92+
93+
app.use(compression());
94+
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
95+
96+
app.use(logger(config.httplog, {
97+
stream: {
98+
write: message => {
99+
message = (message || '').toString();
100+
if (message && process.NODE_ENV !== 'production') {
101+
log.info('HTTP', message.replace('\n', '').trim());
102+
}
103+
}
104+
}
105+
}));
106+
107+
app.use(cookieParser());
108+
app.use(express.static(path.join(__dirname, 'public')));
109+
110+
app.use(session({
111+
store: new RedisStore(config.redis),
112+
secret: config.secret,
113+
saveUninitialized: false,
114+
resave: false
115+
}));
116+
app.use(flash());
117+
118+
app.use(bodyParser.urlencoded({
119+
extended: true,
120+
limit: config.maxPostSize
121+
}));
122+
123+
app.use(bodyParser.text({
124+
limit: config.maxPostSize
125+
}));
126+
127+
app.use(bodyParser.json({
128+
limit: config.maxPostSize
129+
}));
130+
131+
// make sure flash messages are available
132+
app.use((req, res, next) => {
133+
res.locals.flash = req.flash.bind(req);
134+
135+
let menu = [
136+
/*{
137+
title: 'Home',
138+
url: '/',
139+
selected: true
140+
}*/
141+
];
142+
143+
res.setSelectedMenu = key => {
144+
menu.forEach(item => {
145+
item.selected = (item.key === key);
146+
});
147+
};
148+
149+
res.locals.menu = menu;
150+
151+
next();
152+
});
153+
154+
// setup HTTP auth
155+
app.use((req, res, next) => {
156+
if (!config.auth) {
157+
return next();
158+
}
159+
let credentials = auth(req);
160+
if (!credentials || credentials.name !== config.user || credentials.pass !== config.pass) {
161+
res.statusCode = 401;
162+
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
163+
res.end('Access denied');
164+
} else {
165+
next();
166+
}
167+
});
168+
169+
app.use('/', routes);
170+
171+
app.use((err, req, res, next) => {
172+
if (!err) {
173+
return next();
174+
}
175+
res.status(err.statusCode || 500);
176+
res.render('error', {
177+
message: err.message,
178+
error: err
179+
});
180+
});
181+
182+
module.exports = app;

config/default.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
module.exports = {
4+
auth: false, // set to true to enable authentication
5+
user: 'admin',
6+
pass: 'secretpass',
7+
8+
port: 8082,
9+
host: '127.0.0.1', // set to false to listen on all interfaces
10+
proxy: true, // trust proxy headers
11+
httplog: 'dev',
12+
secret: 'a cat',
13+
maxPostSize: '2MB',
14+
apiServer: 'http://localhost:8080', // ZoneMTA API location
15+
// redis is needed to fetch delivery counters
16+
redis: {
17+
host: 'localhost',
18+
port: 6379,
19+
db: 2
20+
},
21+
log: {
22+
level: 'info'
23+
}
24+
};

0 commit comments

Comments
 (0)