refactor frontend code and build pipeline
This commit is contained in:
parent
800f071d6c
commit
00aa1d606a
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
@ -1,4 +1,4 @@
|
|||||||
name: jshint
|
name: jshint_backend
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@ -9,12 +9,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: apt
|
- name: apt
|
||||||
run: sudo apt-get install -y nodejs npm
|
run: sudo apt-get install -y nodejs npm
|
||||||
|
- name: install
|
||||||
- name: npm install
|
|
||||||
run: npm i
|
run: npm i
|
||||||
|
- name: jshint
|
||||||
- name: npm test
|
run: npm run jshint_backend
|
||||||
run: npm test
|
|
17
.github/workflows/jshint_frontend.yml
vendored
Normal file
17
.github/workflows/jshint_frontend.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: jshint_frontend
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: apt
|
||||||
|
run: sudo apt-get install -y nodejs npm
|
||||||
|
- name: install
|
||||||
|
run: npm i
|
||||||
|
- name: jshint
|
||||||
|
run: npm run jshint_frontend
|
29
Dockerfile
29
Dockerfile
@ -1,27 +1,30 @@
|
|||||||
# Stage 1 testing
|
# Stage 1 testing
|
||||||
FROM node:13.13.0-alpine
|
FROM node:13.13.0-alpine as builder
|
||||||
|
|
||||||
COPY package.json /data/
|
COPY . /data
|
||||||
COPY package-lock.json /data/
|
|
||||||
COPY src /data/src
|
|
||||||
COPY public /data/public
|
|
||||||
COPY .git/refs/heads/master /data/public/version.txt
|
|
||||||
|
|
||||||
RUN cd /data && npm i && npm test
|
# build
|
||||||
|
RUN cd /data &&\
|
||||||
|
npm ci &&\
|
||||||
|
npm test &&\
|
||||||
|
npm run jshint_backend &&\
|
||||||
|
npm run jshint_frontend &&\
|
||||||
|
npm run bundle
|
||||||
|
|
||||||
# Stage 2 package
|
# Stage 2 package
|
||||||
FROM node:13.13.0-alpine
|
FROM node:13.13.0-alpine
|
||||||
|
|
||||||
COPY package.json /data/
|
COPY . /data
|
||||||
COPY package-lock.json /data/
|
RUN apk update && apk add curl
|
||||||
COPY src /data/src
|
|
||||||
COPY public /data/public
|
|
||||||
COPY .git/refs/heads/master /data/public/version.txt
|
|
||||||
|
|
||||||
RUN cd /data && npm i --only=production
|
RUN cd /data && npm ci --only=production
|
||||||
|
COPY --from=builder /data/public /data/public
|
||||||
|
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=5s --timeout=3s \
|
||||||
|
CMD curl -f http://localhost:8080/ || exit 1
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -397,6 +397,13 @@
|
|||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.6",
|
"version": "7.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
@ -697,6 +704,15 @@
|
|||||||
"string_decoder": "~0.10.x"
|
"string_decoder": "~0.10.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rollup": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-1RhFDRJeg027YjBO6+JxmVWkEZY0ASztHhoEUEWxOwkh4mjO58TFD6Uo7T7Y3FbmDpRTfKhM5NVxJyimCn0Elg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fsevents": "~2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
10
package.json
10
package.json
@ -4,8 +4,11 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "cd src && jshint . && cd ../public/js/ && jshint .",
|
"test": "echo ok",
|
||||||
"start": "node src/index.js"
|
"jshint_backend": "cd src && jshint .",
|
||||||
|
"jshint_frontend": "cd public/js && jshint .",
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"bundle": "cd public/js && rollup -c rollup.config.js"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -15,6 +18,7 @@
|
|||||||
"jsonwebtoken": "^8.4.0"
|
"jsonwebtoken": "^8.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jshint": "^2.10.3"
|
"jshint": "^2.10.3",
|
||||||
|
"rollup": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,6 @@
|
|||||||
|
|
||||||
<script src="js/lib/mithril.min.js"></script>
|
<script src="js/lib/mithril.min.js"></script>
|
||||||
<script src="js/lib/moment.js"></script>
|
<script src="js/lib/moment.js"></script>
|
||||||
<script src="js/state.js"></script>
|
<script src="js/bootstrap.js"></script>
|
||||||
<script src="js/api.js"></script>
|
|
||||||
<script src="js/service.js"></script>
|
|
||||||
<script src="js/nav.js"></script>
|
|
||||||
<script src="js/login.js"></script>
|
|
||||||
<script src="js/messages.js"></script>
|
|
||||||
<script src="js/message_detail.js"></script>
|
|
||||||
<script src="js/compose.js"></script>
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
2
public/js/.gitignore
vendored
Normal file
2
public/js/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
bundle.js
|
||||||
|
bundle.js.map
|
@ -4,7 +4,6 @@
|
|||||||
"esversion": 6,
|
"esversion": 6,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"globals": {
|
"globals": {
|
||||||
"webmail": true,
|
|
||||||
"moment": true,
|
"moment": true,
|
||||||
"m": true
|
"m": true
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
(function(){
|
import state from './state.js';
|
||||||
|
|
||||||
var api = {};
|
export const fetchMails = function(){
|
||||||
|
|
||||||
api.fetchMails = function(){
|
|
||||||
return m.request({
|
return m.request({
|
||||||
url: "api/inbox",
|
url: "api/inbox",
|
||||||
headers: { "authorization": webmail.token }
|
headers: { "authorization": state.token }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.deleteMail = function(index){
|
export const deleteMail = function(index){
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "api/inbox/" + index,
|
url: "api/inbox/" + index,
|
||||||
headers: { "authorization": webmail.token }
|
headers: { "authorization": state.token }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.markRead = function(index){
|
export const markRead = function(index){
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "api/markread",
|
url: "api/markread",
|
||||||
data: { index: index },
|
data: { index: index },
|
||||||
headers: { "authorization": webmail.token }
|
headers: { "authorization": state.token }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.sendMail = function(recipient, subject, text){
|
export const sendMail = function(recipient, subject, text){
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "api/send",
|
url: "api/send",
|
||||||
@ -35,28 +33,21 @@ api.sendMail = function(recipient, subject, text){
|
|||||||
subject: subject,
|
subject: subject,
|
||||||
text: text
|
text: text
|
||||||
},
|
},
|
||||||
headers: { "authorization": webmail.token }
|
headers: { "authorization": state.token }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.verifyToken = function(){
|
export const verifyToken = function(){
|
||||||
return m.request({
|
return m.request({
|
||||||
url: "api/verify",
|
url: "api/verify",
|
||||||
headers: { "authorization": webmail.token }
|
headers: { "authorization": state.token }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.login = function(username, password){
|
export const login = function(username, password){
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "api/login",
|
url: "api/login",
|
||||||
data: { username: username, password: password }
|
data: { username: username, password: password }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//publish
|
|
||||||
window.webmail.api = api;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
16
public/js/bootstrap.js
vendored
Normal file
16
public/js/bootstrap.js
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
(function(){
|
||||||
|
var s = document.createElement("script");
|
||||||
|
|
||||||
|
if (location.host === "127.0.0.1:8080") {
|
||||||
|
//dev
|
||||||
|
s.setAttribute("src", "js/main.js");
|
||||||
|
s.setAttribute("type", "module");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//prod
|
||||||
|
s.setAttribute("src", "js/bundle.js");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(s);
|
||||||
|
})();
|
@ -1,61 +1,55 @@
|
|||||||
(function(){
|
import state from './state.js';
|
||||||
|
import { sendMail } from './service.js';
|
||||||
|
|
||||||
var state = webmail.compose;
|
const Compose = {
|
||||||
|
view: function(){
|
||||||
|
return [
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("input[type=text]", {
|
||||||
|
class:"form-control",
|
||||||
|
placeholder:"Recipient",
|
||||||
|
value: state.compose.recipient,
|
||||||
|
oninput: function(e){ state.compose.recipient = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("input[type=text]", {
|
||||||
|
class:"form-control",
|
||||||
|
placeholder:"Subject",
|
||||||
|
value: state.compose.subject,
|
||||||
|
oninput: function(e){ state.compose.subject = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("textarea", {
|
||||||
|
class:"form-control",
|
||||||
|
placeholder:"Text",
|
||||||
|
style: "height: 300px;",
|
||||||
|
value: state.compose.body,
|
||||||
|
oninput: function(e){ state.compose.body = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("button[type=submit]", {
|
||||||
|
class:"btn btn-sm btn-block btn-primary",
|
||||||
|
onclick: sendMail,
|
||||||
|
disabled: !state.compose.body || !state.compose.subject || !state.compose.recipient
|
||||||
|
}, "Submit")
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var Compose = {
|
export default {
|
||||||
view: function(){
|
view: function(){
|
||||||
return [
|
if (state.loginState.loggedIn)
|
||||||
m("div", {class:"row"}, [
|
return m("div", {class:"row"}, [
|
||||||
m("input[type=text]", {
|
m("div", {class:"col-md-2"}),
|
||||||
class:"form-control",
|
m("form", {class:"col-md-8"}, m(Compose)),
|
||||||
placeholder:"Recipient",
|
m("div", {class:"col-md-2"})
|
||||||
value: state.recipient,
|
]);
|
||||||
oninput: function(e){ state.recipient = e.target.value; }
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
m("input[type=text]", {
|
|
||||||
class:"form-control",
|
|
||||||
placeholder:"Subject",
|
|
||||||
value: state.subject,
|
|
||||||
oninput: function(e){ state.subject = e.target.value; }
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
m("textarea", {
|
|
||||||
class:"form-control",
|
|
||||||
placeholder:"Text",
|
|
||||||
style: "height: 300px;",
|
|
||||||
value: state.body,
|
|
||||||
oninput: function(e){ state.body = e.target.value; }
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
m("button[type=submit]", {
|
|
||||||
class:"btn btn-sm btn-block btn-primary",
|
|
||||||
onclick: webmail.service.sendMail,
|
|
||||||
disabled: !state.body || !state.subject || !state.recipient
|
|
||||||
}, "Submit")
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
else
|
||||||
webmail.routes["/compose"] = {
|
return null;
|
||||||
view: function(){
|
}
|
||||||
if (webmail.loginState.loggedIn)
|
};
|
||||||
return m("div", {class:"row"}, [
|
|
||||||
m("div", {class:"col-md-2"}),
|
|
||||||
m("form", {class:"col-md-8"}, m(Compose)),
|
|
||||||
m("div", {class:"col-md-2"})
|
|
||||||
]);
|
|
||||||
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
@ -1,67 +1,64 @@
|
|||||||
(function(){
|
import state from './state.js';
|
||||||
|
import { login, logout } from './service.js';
|
||||||
|
|
||||||
var state = webmail.loginState;
|
var LoginButton = function(){
|
||||||
|
|
||||||
var LoginButton = function(){
|
var infoText = m("span", {class:"badge badge-light"}, state.loginState.errorMsg ? state.loginState.errorMsg : "");
|
||||||
|
var spinner = state.loginState.busy ? m("i", {class:"fas fa-spinner fa-spin"}) : null;
|
||||||
|
|
||||||
var infoText = m("span", {class:"badge badge-light"}, state.errorMsg ? state.errorMsg : "");
|
return m("button", {
|
||||||
var spinner = state.busy ? m("i", {class:"fas fa-spinner fa-spin"}) : null;
|
class:"btn btn-sm btn-block btn-primary",
|
||||||
|
disabled: !state.loginState.username || !state.loginState.password,
|
||||||
|
onclick: function(){ login(state.loginState.username, state.loginState.password); }
|
||||||
|
}, [spinner, " Login ", infoText]);
|
||||||
|
};
|
||||||
|
|
||||||
return m("button", {
|
var LogoutButton = function(){
|
||||||
class:"btn btn-sm btn-block btn-primary",
|
return m("button[type=submit]", {
|
||||||
disabled: !state.username || !state.password,
|
class:"btn btn-sm btn-block btn-secondary",
|
||||||
onclick: function(){ webmail.service.login(state.username, state.password); }
|
onclick: logout
|
||||||
}, [spinner, " Login ", infoText]);
|
}, "Logout");
|
||||||
};
|
};
|
||||||
|
|
||||||
var LogoutButton = function(){
|
var LoginForm = {
|
||||||
return m("button[type=submit]", {
|
view: function(){
|
||||||
class:"btn btn-sm btn-block btn-secondary",
|
return [
|
||||||
onclick: webmail.service.logout
|
m("div", {class:"row"}, [
|
||||||
}, "Logout");
|
m("h3", "Webmail login")
|
||||||
};
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("input[type=text]", {
|
||||||
|
class:"form-control",
|
||||||
|
placeholder:"Playername",
|
||||||
|
disabled: state.loginState.loggedIn,
|
||||||
|
value: state.loginState.username,
|
||||||
|
oninput: function(e){ state.loginState.username = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
m("input[type=password]", {
|
||||||
|
class:"form-control",
|
||||||
|
placeholder:"Password",
|
||||||
|
disabled: state.loginState.loggedIn,
|
||||||
|
value: state.loginState.password,
|
||||||
|
oninput: function(e){ state.loginState.password = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m("div", {class:"row"}, [
|
||||||
|
state.loginState.loggedIn ? LogoutButton() : LoginButton()
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var LoginForm = {
|
export default {
|
||||||
view: function(){
|
view: function(){
|
||||||
return [
|
return [
|
||||||
m("div", {class:"row"}, [
|
m("div", {class:"row"}, [
|
||||||
m("h3", "Webmail login")
|
m("div", {class:"col-md-4"}),
|
||||||
]),
|
m("form", {class:"col-md-4"}, m(LoginForm)),
|
||||||
m("div", {class:"row"}, [
|
m("div", {class:"col-md-4"})
|
||||||
m("input[type=text]", {
|
])
|
||||||
class:"form-control",
|
];
|
||||||
placeholder:"Playername",
|
}
|
||||||
disabled: state.loggedIn,
|
};
|
||||||
value: state.username,
|
|
||||||
oninput: function(e){ state.username = e.target.value; }
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
m("input[type=password]", {
|
|
||||||
class:"form-control",
|
|
||||||
placeholder:"Password",
|
|
||||||
disabled: state.loggedIn,
|
|
||||||
value: state.password,
|
|
||||||
oninput: function(e){ state.password = e.target.value; }
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
state.loggedIn ? LogoutButton() : LoginButton()
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
webmail.routes["/login"] = {
|
|
||||||
view: function(){
|
|
||||||
return [
|
|
||||||
m("div", {class:"row"}, [
|
|
||||||
m("div", {class:"col-md-4"}),
|
|
||||||
m("form", {class:"col-md-4"}, m(LoginForm)),
|
|
||||||
m("div", {class:"col-md-4"})
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
(function(){
|
import routes from './routes.js';
|
||||||
|
import Nav from './nav.js';
|
||||||
|
|
||||||
m.route(document.getElementById("app"), "/login", webmail.routes);
|
m.route(document.getElementById("app"), "/login", routes);
|
||||||
|
m.mount(document.getElementById("nav"), Nav);
|
||||||
})();
|
|
||||||
|
@ -1,41 +1,40 @@
|
|||||||
(function(){
|
import state from './state.js';
|
||||||
|
import { readMail, reply } from './service.js';
|
||||||
|
|
||||||
webmail.routes["/message/:id"] = {
|
export default {
|
||||||
view: function(){
|
view: function(){
|
||||||
if (!webmail.mails)
|
if (!state.mails)
|
||||||
return m("div", "Loading...");
|
return m("div", "Loading...");
|
||||||
|
|
||||||
var id = m.route.param("id");
|
var id = m.route.param("id");
|
||||||
var mail = webmail.service.readMail(id);
|
var mail = readMail(id);
|
||||||
|
|
||||||
var timeStr = "";
|
var timeStr = "";
|
||||||
|
|
||||||
if (mail.time){
|
if (mail.time){
|
||||||
var time_m = moment(mail.time * 1000);
|
var time_m = moment(mail.time * 1000);
|
||||||
var durationStr = moment.duration(time_m - moment()).humanize(true);
|
var durationStr = moment.duration(time_m - moment()).humanize(true);
|
||||||
|
|
||||||
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
|
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
|
||||||
}
|
|
||||||
|
|
||||||
var body = [];
|
|
||||||
|
|
||||||
mail.body.split("\n").forEach(function(line){
|
|
||||||
body.push(line);
|
|
||||||
body.push( m("br") );
|
|
||||||
});
|
|
||||||
|
|
||||||
var replyBtn = m("button[type=button]", {
|
|
||||||
onclick: function(){ webmail.service.reply(id); },
|
|
||||||
class: "btn btn-sm btn-primary"
|
|
||||||
}, "Reply");
|
|
||||||
|
|
||||||
return [
|
|
||||||
m("h2", mail.subject),
|
|
||||||
m("h5", [ "From: ", m("b", mail.sender), replyBtn ]),
|
|
||||||
m("h5", [ "Sent: ", m("b", timeStr) ]),
|
|
||||||
m("div", body)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
var body = [];
|
||||||
|
|
||||||
|
mail.body.split("\n").forEach(function(line){
|
||||||
|
body.push(line);
|
||||||
|
body.push( m("br") );
|
||||||
|
});
|
||||||
|
|
||||||
|
var replyBtn = m("button[type=button]", {
|
||||||
|
onclick: function(){ reply(id); },
|
||||||
|
class: "btn btn-sm btn-primary"
|
||||||
|
}, "Reply");
|
||||||
|
|
||||||
|
return [
|
||||||
|
m("h2", mail.subject),
|
||||||
|
m("h5", [ "From: ", m("b", mail.sender), replyBtn ]),
|
||||||
|
m("h5", [ "Sent: ", m("b", timeStr) ]),
|
||||||
|
m("div", body)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,79 +1,75 @@
|
|||||||
(function(){
|
import state from './state.js';
|
||||||
|
import { deleteMail } from './service.js';
|
||||||
|
|
||||||
|
var InboxRow = {
|
||||||
|
view: function(vnode){
|
||||||
|
function openMail(){
|
||||||
|
m.route.set("/message/:id", { id: vnode.attrs.row.index });
|
||||||
|
}
|
||||||
|
|
||||||
var InboxRow = {
|
var timeStr = "";
|
||||||
view: function(vnode){
|
|
||||||
function openMail(){
|
|
||||||
m.route.set("/message/:id", { id: vnode.attrs.row.index });
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteMail(){
|
if (vnode.attrs.row.time){
|
||||||
webmail.service.deleteMail(vnode.attrs.row.index);
|
var time_m = moment(vnode.attrs.row.time * 1000);
|
||||||
}
|
var durationStr = moment.duration(time_m - moment()).humanize(true);
|
||||||
|
|
||||||
var timeStr = "";
|
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
|
||||||
|
}
|
||||||
|
|
||||||
if (vnode.attrs.row.time){
|
var rowClass = vnode.attrs.row.unread ? "table-primary" : "";
|
||||||
var time_m = moment(vnode.attrs.row.time * 1000);
|
|
||||||
var durationStr = moment.duration(time_m - moment()).humanize(true);
|
|
||||||
|
|
||||||
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
|
return m("tr", {class: rowClass}, [
|
||||||
}
|
m("td", vnode.attrs.row.sender),
|
||||||
|
m("td", vnode.attrs.row.subject),
|
||||||
var rowClass = vnode.attrs.row.unread ? "table-primary" : "";
|
m("td", timeStr),
|
||||||
|
m("td", [
|
||||||
return m("tr", {class: rowClass}, [
|
m("div", { class: "btn-group" }, [
|
||||||
m("td", vnode.attrs.row.sender),
|
m("button[type=button]", { class: "btn btn-primary", onclick: openMail },[
|
||||||
m("td", vnode.attrs.row.subject),
|
m("i", {class:"fa fa-envelope-open"}),
|
||||||
m("td", timeStr),
|
"Open"
|
||||||
m("td", [
|
]),
|
||||||
m("div", { class: "btn-group" }, [
|
m("button[type=button]", {
|
||||||
m("button[type=button]", { class: "btn btn-primary", onclick: openMail },[
|
class: "btn btn-danger",
|
||||||
m("i", {class:"fa fa-envelope-open"}),
|
onclick: () => deleteMail(vnode.attrs.row.index)
|
||||||
"Open"
|
},[
|
||||||
]),
|
m("i", {class:"fa fa-trash"}),
|
||||||
m("button[type=button]", { class: "btn btn-danger", onclick: deleteMail },[
|
"Remove"
|
||||||
m("i", {class:"fa fa-trash"}),
|
|
||||||
"Remove"
|
|
||||||
])
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var InboxTable = {
|
||||||
|
view: function(){
|
||||||
|
if (!state.mails){
|
||||||
|
return m("div", "Loading...");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
var InboxTable = {
|
var head = m("thead", m("tr", [
|
||||||
view: function(){
|
m("th", "Sender"),
|
||||||
if (!webmail.mails){
|
m("th", "Subject"),
|
||||||
return m("div", "Loading...");
|
m("th", "Sent"),
|
||||||
}
|
m("th", "Action")
|
||||||
|
]));
|
||||||
|
|
||||||
var head = m("thead", m("tr", [
|
var body = m("tbody", state.mails.map(function(row){
|
||||||
m("th", "Sender"),
|
return m(InboxRow, {row: row});
|
||||||
m("th", "Subject"),
|
}));
|
||||||
m("th", "Sent"),
|
|
||||||
m("th", "Action")
|
|
||||||
]));
|
|
||||||
|
|
||||||
var body = m("tbody", webmail.mails.map(function(row){
|
return m("table",
|
||||||
return m(InboxRow, {row: row});
|
{class:"table table-condensed table-striped table-sm"},
|
||||||
}));
|
[head, body]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return m("table",
|
export default {
|
||||||
{class:"table table-condensed table-striped table-sm"},
|
view: function(){
|
||||||
[head, body]
|
if (state.loginState.loggedIn)
|
||||||
);
|
return m(InboxTable);
|
||||||
}
|
else
|
||||||
};
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
webmail.routes["/messages"] = {
|
|
||||||
view: function(){
|
|
||||||
if (webmail.loginState.loggedIn)
|
|
||||||
return m(InboxTable);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
@ -1,37 +1,34 @@
|
|||||||
|
import state from './state.js';
|
||||||
|
import { countUnread } from './service.js';
|
||||||
|
|
||||||
(function(){
|
function NavLinks(){
|
||||||
|
|
||||||
function NavLinks(){
|
var links = [];
|
||||||
|
|
||||||
var links = [];
|
links.push( m("a", {class:"nav-link", href:"#!/login"}, "Login") );
|
||||||
|
|
||||||
links.push( m("a", {class:"nav-link", href:"#!/login"}, "Login") );
|
if (state.loginState.loggedIn){
|
||||||
|
links.push( m("a", {class:"nav-link", href:"#!/messages"}, [
|
||||||
if (webmail.loginState.loggedIn){
|
"Messages",
|
||||||
links.push( m("a", {class:"nav-link", href:"#!/messages"}, [
|
m("span", {class: "badge badge-light"}, countUnread())
|
||||||
"Messages",
|
])
|
||||||
m("span", {class: "badge badge-light"}, webmail.service.countUnread())
|
);
|
||||||
])
|
links.push( m("a", {class:"nav-link", href:"#!/compose"}, "Compose") );
|
||||||
);
|
|
||||||
links.push( m("a", {class:"nav-link", href:"#!/compose"}, "Compose") );
|
|
||||||
}
|
|
||||||
|
|
||||||
return m("ul", {class:"navbar-nav"}, links);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavBarContent(){
|
return m("ul", {class:"navbar-nav"}, links);
|
||||||
return m("div", {class:"container"}, [
|
}
|
||||||
m("i", {class:"fa fa-envelope"}),
|
|
||||||
m("a", {class:"navbar-brand", href:"#"}, "Minetest webmail"),
|
function NavBarContent(){
|
||||||
m("div", {class:"navbar-collapse"}, NavLinks())
|
return m("div", {class:"container"}, [
|
||||||
]);
|
m("i", {class:"fa fa-envelope"}),
|
||||||
|
m("a", {class:"navbar-brand", href:"#"}, "Minetest webmail"),
|
||||||
|
m("div", {class:"navbar-collapse"}, NavLinks())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
view: function(){
|
||||||
|
return m("nav", {class:"navbar navbar-dark bg-dark fixed-top navbar-expand-lg"}, NavBarContent());
|
||||||
}
|
}
|
||||||
|
};
|
||||||
m.mount(document.getElementById("nav"), {
|
|
||||||
view: function(){
|
|
||||||
return m("nav", {class:"navbar navbar-dark bg-dark fixed-top navbar-expand-lg"}, NavBarContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
10
public/js/rollup.config.js
Normal file
10
public/js/rollup.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
export default [{
|
||||||
|
input: 'main.js',
|
||||||
|
output: {
|
||||||
|
file :'bundle.js',
|
||||||
|
format: 'umd',
|
||||||
|
sourcemap: true,
|
||||||
|
compact: true
|
||||||
|
}
|
||||||
|
}];
|
12
public/js/routes.js
Normal file
12
public/js/routes.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import Login from './login.js';
|
||||||
|
import Compose from './compose.js';
|
||||||
|
import Messages from './messages.js';
|
||||||
|
import MessageDetail from './message_detail.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
"/login": Login,
|
||||||
|
"/compose": Compose,
|
||||||
|
"/messages": Messages,
|
||||||
|
"/message/:id": MessageDetail
|
||||||
|
};
|
@ -1,73 +1,79 @@
|
|||||||
(function(state){
|
import state from './state.js';
|
||||||
|
import {
|
||||||
var service = {};
|
markRead,
|
||||||
|
verifyToken,
|
||||||
|
login as api_login,
|
||||||
|
fetchMails as api_fetchMails,
|
||||||
|
sendMail as api_sendMail,
|
||||||
|
deleteMail as api_deleteMail
|
||||||
|
} from './api.js';
|
||||||
|
|
||||||
//verify token if available
|
//verify token if available
|
||||||
if (webmail.token){
|
if (state.token){
|
||||||
webmail.api.verifyToken()
|
verifyToken()
|
||||||
.then(function(result){
|
.then(function(result){
|
||||||
if (result.username){
|
if (result.username){
|
||||||
state.username = result.username;
|
state.loginState.username = result.loginState.username;
|
||||||
state.loggedIn = true;
|
state.loginState.loggedIn = true;
|
||||||
//fetch messages after token alright
|
//fetch messages after token alright
|
||||||
service.fetchMails();
|
fetchMails();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
service.login = function(username, password){
|
export const login = function(username, password){
|
||||||
if (!username || !password)
|
if (!username || !password)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
state.errorMsg = "";
|
state.loginState.errorMsg = "";
|
||||||
state.busy = true;
|
state.loginState.busy = true;
|
||||||
|
|
||||||
webmail.api.login(username, password)
|
api_login(username, password)
|
||||||
.then(function(result){
|
.then(function(result){
|
||||||
state.busy = false;
|
state.loginState.busy = false;
|
||||||
if (result.success){
|
if (result.success){
|
||||||
state.loggedIn = true;
|
state.loginState.loggedIn = true;
|
||||||
state.errorMsg = "";
|
state.loginState.errorMsg = "";
|
||||||
|
|
||||||
//save token
|
//save token
|
||||||
webmail.token = result.token;
|
state.token = result.token;
|
||||||
localStorage["webmail-token"] = result.token;
|
localStorage["webmail-token"] = result.token;
|
||||||
|
|
||||||
//fetch mails after login
|
//fetch mails after login
|
||||||
service.fetchMails();
|
fetchMails();
|
||||||
} else {
|
} else {
|
||||||
state.errorMsg = "Login failed: " + result.message;
|
state.loginState.errorMsg = "Login failed: " + result.message;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function(){
|
.catch(function(){
|
||||||
state.errorMsg = "System error!";
|
state.loginState.errorMsg = "System error!";
|
||||||
state.busy = false;
|
state.loginState.busy = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
service.logout = function(){
|
export const logout = function(){
|
||||||
state.loggedIn = false;
|
state.loginState.loggedIn = false;
|
||||||
webmail.mails = [];
|
state.mails = [];
|
||||||
|
|
||||||
//clear token
|
//clear token
|
||||||
webmail.token = null;
|
state.token = null;
|
||||||
delete localStorage["webmail-token"];
|
delete localStorage["webmail-token"];
|
||||||
};
|
};
|
||||||
|
|
||||||
service.fetchMails = function(){
|
export const fetchMails = function(){
|
||||||
if (!webmail.mails || !webmail.mails.length){
|
if (!state.mails || !state.mails.length){
|
||||||
webmail.api.fetchMails()
|
api_fetchMails()
|
||||||
.then(function(result){
|
.then(function(result){
|
||||||
webmail.mails = result;
|
state.mails = result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
service.countUnread = function(){
|
export const countUnread = function(){
|
||||||
var count = 0;
|
var count = 0;
|
||||||
if (webmail.mails && webmail.mails.length){
|
if (state.mails && state.mails.length){
|
||||||
webmail.mails.forEach(function(mail){
|
state.mails.forEach(function(mail){
|
||||||
if (mail.unread)
|
if (mail.unread)
|
||||||
count++;
|
count++;
|
||||||
});
|
});
|
||||||
@ -76,29 +82,23 @@ service.countUnread = function(){
|
|||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.sendMail = function(){
|
export const sendMail = function(){
|
||||||
webmail.api.sendMail(webmail.compose.recipient, webmail.compose.subject, webmail.compose.body);
|
api_sendMail(state.compose.recipient, state.compose.subject, state.compose.body);
|
||||||
webmail.compose.recipient = "";
|
state.compose.recipient = "";
|
||||||
webmail.compose.subject = "";
|
state.compose.subject = "";
|
||||||
webmail.compose.body = "";
|
state.compose.body = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
service.reply = function(index){
|
|
||||||
var mail = service.readMail(index);
|
|
||||||
webmail.compose.recipient = mail.sender;
|
|
||||||
webmail.compose.subject = "Re: " + mail.subject;
|
|
||||||
webmail.compose.body = "\n---- Original message ----\n" + mail.body;
|
|
||||||
m.route.set("/compose");
|
|
||||||
};
|
|
||||||
|
|
||||||
service.readMail = function(index){
|
|
||||||
if (webmail.mails && webmail.mails.length){
|
|
||||||
|
|
||||||
var mail = webmail.mails[index-1];
|
export const readMail = function(index){
|
||||||
|
if (state.mails && state.mails.length){
|
||||||
|
|
||||||
|
var mail = state.mails[index-1];
|
||||||
|
|
||||||
//mark as read with api
|
//mark as read with api
|
||||||
if (mail.unread){
|
if (mail.unread){
|
||||||
webmail.api.markRead(index);
|
markRead(index);
|
||||||
|
|
||||||
//mark read locally
|
//mark read locally
|
||||||
mail.unread = false;
|
mail.unread = false;
|
||||||
@ -108,11 +108,20 @@ service.readMail = function(index){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
service.deleteMail = function(index){
|
export const reply = function(index){
|
||||||
return webmail.api.deleteMail(index)
|
var mail = readMail(index);
|
||||||
|
state.compose.recipient = mail.sender;
|
||||||
|
state.compose.subject = "Re: " + mail.subject;
|
||||||
|
state.compose.body = "\n---- Original message ----\n" + mail.body;
|
||||||
|
m.route.set("/compose");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const deleteMail = function(index){
|
||||||
|
return api_deleteMail(index)
|
||||||
.then(function(){
|
.then(function(){
|
||||||
var new_index = 1;
|
var new_index = 1;
|
||||||
webmail.mails = webmail.mails
|
state.mails = state.mails
|
||||||
.filter(function(mail){ return mail.index != index; })
|
.filter(function(mail){ return mail.index != index; })
|
||||||
.map(function(mail){
|
.map(function(mail){
|
||||||
mail.index = new_index++;
|
mail.index = new_index++;
|
||||||
@ -120,9 +129,3 @@ service.deleteMail = function(index){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
webmail.service = service;
|
|
||||||
|
|
||||||
})(webmail.loginState);
|
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
(function(){
|
|
||||||
|
|
||||||
var webmail = {
|
const state = {
|
||||||
routes: {},
|
routes: {},
|
||||||
token: localStorage["webmail-token"],
|
token: localStorage["webmail-token"],
|
||||||
loginState: {
|
loginState: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
errorMsg: "",
|
errorMsg: "",
|
||||||
busy: false
|
busy: false
|
||||||
},
|
},
|
||||||
compose: {
|
compose: {
|
||||||
recipient: "",
|
recipient: "",
|
||||||
subject: "",
|
subject: "",
|
||||||
body: ""
|
body: ""
|
||||||
},
|
},
|
||||||
mails: null
|
mails: null
|
||||||
};
|
};
|
||||||
|
|
||||||
//publish
|
export default state;
|
||||||
window.webmail = webmail;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user