Rewriting frontend in react, refactoring of routes.

This commit is contained in:
Gabriel Simmer 2019-07-13 20:19:28 -07:00
parent 88e0d6c7b1
commit 9ab5f248a9
31 changed files with 11177 additions and 469 deletions

6
.gitignore vendored
View file

@ -18,4 +18,8 @@
config.json
# Binary
nas
nas
*.db
.lock
assets/web/*

View file

@ -1,52 +1 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="utf-8" http-equiv="encoding">
<title>Home | GoNAS</title>
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
<main>
<div>
<h1 style="display: inline-block">GoNAS</h1>
<p>Hot Storage Usage (<span id="hotUsagePercent"></span>%)</p>
<progress max="100" id="hotUsage"></progress>
<p>Cold Storage Usage (<span id="coldUsagePercent"></span>%)</p>
<progress max="100" id="coldUsage"></progress>
</div>
<div class="content index">
<a href="/files/">
<img src="/assets/svgs/hotfiles.svg" alt="">Hot Files</a><br>
<a href="/archive/">
<img src="/assets/svgs/coldfiles.svg" alt="">Cold Files</a>
</div>
</main>
<script>
const $ = selector => document.querySelector(selector);
const init = event => {
getUsages();
}
const getUsages = () => {
fetch('/api/diskusage')
.then(function (response) {
return response.json();
})
.then(function (jsonResult) {
console.log(jsonResult);
let hot = (jsonResult.HotStorage.Free) / (jsonResult.HotStorage.Total) * 100;
let cold = (jsonResult.ColdStorage.Free) / (jsonResult.ColdStorage.Total) * 100;
// Flip values to reflect % used rather than % free.
$("#hotUsage").value = 100 - hot;
$("#hotUsagePercent").innerText = Math.floor(100 - hot);
$("#coldUsage").value = 100 - cold;
$("#coldUsagePercent").innerText = Math.floor(100 - cold);
});
}
window.addEventListener('load', init);
</script>
</body>
</html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>React App</title><link href="/static/css/main.feacb500.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],p[t]&&i.push(p[t][0]),p[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),a()}function a(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==p[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},p={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var r=window.webpackJsonp=window.webpackJsonp||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;a()}([])</script><script src="/static/js/2.585c3ac0.chunk.js"></script><script src="/static/js/main.f266202f.chunk.js"></script></body></html>

View file

@ -1,41 +0,0 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="utf-8" http-equiv="encoding">
<title>| GoNAS</title>
<link rel="stylesheet" href="/assets/styles.css">
<script type="text/javascript" src="/assets/app.js"></script>
</head>
<body>
<div id="uploadoverlay" class="fileupload hidden">
<p><img src="/assets/svgs/newfile.svg" alt="">Upload</p>
</div>
<header>
<a href="/" class="back-button">
<img src="/assets/svgs/back.svg" alt="" data-dismiss-context="false">
</a>
<h1 style="display: inline-block">GoNAS</h1>
</header>
<main>
<div>
<h1 id="path-header"></h1>
</div>
<div class="context-menu hidden" id="context" data-dismiss-context="false">
<ul class="context-actions">
<li>Archive</li>
<li>Get MD5</li>
</ul>
</div>
<div class="content" id="filelist">
<form action="/upload" method="post" enctype="multipart/form-data">
<input id="path" type="value" name="path">
<input type="file" multiple name="file">
<input type="submit" value="Upload">
</form>
<p class="directory"><a href="#" id="previous-dir">..</a></p>
</div>
</main>
</body>
</html>

View file

@ -1,87 +0,0 @@
const $ = selector => document.querySelector(selector);
const init = event => {
var path = window.location.pathname;
var pathSplit = path.split("/");
var path = pathSplit.splice(1).join("/");
document.title = path + " | PiNAS";
getFiles(pathSplit[0], path, json => {
console.log(json);
document.getElementById("previous-dir").href = "/" + json.Prefix + "/" + json.Previous;
buildList(json);
});
$("#path-header").innerText = path;
$("#path").value = path;
document.querySelector("body").addEventListener('contextmenu', function (ev) {
if (ev.target.localName == "a") {
ev.preventDefault();
var d = document.getElementById('context');
d.classList.remove("hidden");
d.style.position = "absolute";
d.style.left = ev.clientX + 'px';
d.style.top = ev.clientY + 'px';
}
return false;
}, false);
$("body").addEventListener('click', function (ev) {
let shouldDismiss = ev.target.dataset.dismissContext == undefined && ev.target.parentElement.classList.contains("context-actions") == false && ev.target.localName != 'a';
if (ev.which == 1 && shouldDismiss) {
ev.preventDefault();
var d = $('#context');
d.classList.add("hidden");
return false;
}
}, false);
}
const getFiles = (prefix, path, callback) => {
fetch('/api/' + prefix + '/' + path)
.then(function (response) {
return response.json();
})
.then(function (jsonResult) {
callback(jsonResult);
});
}
const buildList = data => {
for (var i = 0; i < data.Files.length; i++) {
let fileItem = document.createElement('p');
let fileLink = document.createElement('a');
if (data.Files[i].IsDirectory == true) {
fileItem.classList.add("directory");
fileLink.href = "/" + data.Prefix + "/" + data.Path + "/" + data.Files[i].Name;
} else {
fileItem.classList.add("file");
fileLink.href = "/" + data.SinglePrefix + "/" + data.Path + "/" + data.Files[i].Name;
}
fileLink.innerText = data.Files[i].Name;
fileItem.appendChild(fileLink);
$("#filelist").appendChild(fileItem);
}
}
const upload = (file, path) => {
var formData = new FormData();
formData.append("path", path);
formData.append("file", file);
fetch('/upload', { // Your POST endpoint
method: 'POST',
body: formData
}).then(
success => console.log(success) // Handle the success response object
).catch(
error => console.log(error) // Handle the error response object
);
};
window.addEventListener('load', init);

View file

@ -1,103 +0,0 @@
body {
font-family: Open Sans, Arial, sans-serif;
color: #454545;
font-size: 16px;
background-color: #fefefe;
}
.fileupload {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2;
cursor: pointer;
text-align: center;
color: white;
font-size: 70px;
}
.hidden {
display: none;
}
h1, h2, h3 {
padding: 0;
margin: 0;
}
.content {
margin: 2em auto;
max-width: 800px;
padding: 1em;
line-height: 1.4;
text-align: justify;
}
.directory, .file {
margin: 0;
padding: 0;
}
.directory a, .file a {
color: #454545;
text-decoration: none;
display: block;
width:100%;
border: 1px solid #454545;
margin-top: -1px;
padding: 0.5em;
-webkit-transition: background-color 0.5s linear;
-moz-transition: background-color 0.5s linear;
-ms-transition: background-color 0.5s linear;
-o-transition: background-color 0.5s linear;
transition: background-color 0.5s linear;
}
.directory:hover a , .file:hover a {
background-color: lightgray;
-webkit-transition: background-color 0.5s linear;
-moz-transition: background-color 0.5s linear;
-ms-transition: background-color 0.5s linear;
-o-transition: background-color 0.5s linear;
transition: background-color 0.5s linear;
}
.index {
display: flex;
text-align: center;
flex-direction: column;
font-size: 32px;
}
.index a {
color: #454545;
text-decoration: none;
}
a.back-button {
display: inline-block;
}
.context-menu {
background-color: #ededed;
padding: 10px;
}
.context-actions {
list-style: none;
padding: 0;
margin: 0;
}
.context-actions li {
border-bottom: 1px solid black;
cursor: pointer;
}
progress[value] {
height: 20px;
}

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
<circle cx="12" cy="12" r="10"/>
<path d="M12 8l-4 4 4 4M16 12H9"/>
</svg>

Before

Width:  |  Height:  |  Size: 265 B

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
</svg>

Before

Width:  |  Height:  |  Size: 356 B

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
<path d="M13 2H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-7-7z"/>
<path d="M13 3v6h6"/>
</svg>

Before

Width:  |  Height:  |  Size: 295 B

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
<line x1="22" y1="12" x2="2" y2="12"></line>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
<line x1="6" y1="16" x2="6" y2="16"></line>
<line x1="10" y1="16" x2="10" y2="16"></line>
</svg>

Before

Width:  |  Height:  |  Size: 465 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#ffffff"
stroke-width="1" stroke-linecap="butt" stroke-linejoin="round">
<path d="M20 11.08V8l-6-6H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h6"/>
<path d="M14 3v5h5M18 21v-6M15 18h6"/>
</svg>

Before

Width:  |  Height:  |  Size: 300 B

View file

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="68" stroke="none"
stroke-linecap="round"
stroke-linejoin="round" fill="#fff" fill-rule="evenodd">
<path d="M14.5834 1.088c-.3937.0232-.7687.1763-1.0663.4352-.8802-.4872-1.9694-.3835-2.742.2612-.7377-.2487-1.5523-.0147-2.0456.5875-1.074-.2264-2.1895.11-2.9595.8922-2.176-.261-2.8072 1.2404-2.0456 2.655a1.828 1.828 0 0 0 .1306 2.6114c-.2986.876-.0105 1.845.718 2.4155-.1703.8653.2332 1.7424 1.001 2.176.0573 1.0676.703 2.015 1.6756 2.46.1713 1.0887 1.076 1.912 2.176 1.9803.2973.9598 1.1718 1.6244 2.176 1.654C7.7036 21.025 5.3738 25.1 5.792 29.3778l-.4135.74a9.814 9.814 0 0 0-4.2222 6.9586c-.334 2.8214.5712 5.65 2.4813 7.752.3287 1.4856.78 2.9413 1.3492 4.3523a10.881 10.881 0 0 0 6.7242 8.7045 22.675 22.675 0 0 0 6.5284 3.6559c2.013 2.2464 4.883 3.5352 7.8994 3.547h.1306c3.0162-.012 5.8864-1.3007 7.8993-3.547 2.3636-.8487 4.5697-2.084 6.5284-3.656a10.881 10.881 0 0 0 6.7242-8.7045 27.512 27.512 0 0 0 1.3492-4.3523 9.814 9.814 0 0 0 2.4814-7.752c-.334-2.8213-1.874-5.3595-4.2223-6.9586l-.6963-.74a10.141 10.141 0 0 0-5.7015-10.0102c1.0043-.0294 1.879-.694 2.1762-1.654 1.1-.0682 2.0047-.8916 2.176-1.9802a2.873 2.873 0 0 0 1.6756-2.4591c.768-.4337 1.1713-1.3108 1-2.176a2.176 2.176 0 0 0 .7181-2.4155 1.828 1.828 0 0 0 .615-1.2782c.0245-.4915-.15-.972-.4844-1.3332.7617-1.3927 0-2.8942-2.0456-2.6548a3.221 3.221 0 0 0-3.0683-1.0446c-.4898-.593-1.2926-.8262-2.0238-.5875-.7725-.6447-1.8617-.7484-2.742-.2612-.854-.5707-1.9785-.5267-2.7855.1088-.8154-.29-1.7225-.1323-2.3937.4135a2.459 2.459 0 0 0-2.6984 1.2186h-.8487c-1.8945 1.3177-3.2534 3.2712-3.83 5.5056a9.64 9.64 0 0 0-3.83-5.6361h-.8487a2.459 2.459 0 0 0-2.6984-1.0881 2.481 2.481 0 0 0-2.4372-.4135 5.29 5.29 0 0 0-1.6757-.544z"
fill="#000"/>
<path d="M10.0353 7.03a41.061 41.061 0 0 1 11.5335 7.8123c-.9575 3.9605-6.1585 4.1564-8.0517 4.0476.36-.1187.66-.3775.827-.7182a21.764 21.764 0 0 1-3.3294-.6963 1.11 1.11 0 0 0 .8922-.5223 9.553 9.553 0 0 1-3.0031-1.2404c.4176.065.8443-.028 1.197-.26a11.622 11.622 0 0 1-2.8725-1.7845 2.176 2.176 0 0 0 1.2186-.1958A11.076 11.076 0 0 1 6.14 11.5988c.4.11.8246.0722 1.197-.1088a9.23 9.23 0 0 1-1.9803-2.1761c.4285.2282.9425.2282 1.37 0-.6875-.65-1.294-1.3807-1.8062-2.1762a2.892 2.892 0 0 0 1.2839 0 6.072 6.072 0 0 0-1.1533-2.176c.978.0703 1.9597.0703 2.9377 0l-.718-.8704c1.1-.2 2.214-.086 3.2425.3264.3917-.3047 0-.6964-.4788-1.088a11.796 11.796 0 0 1 2.7202.6746c.4134-.3917-.3047-.74-.653-1.175 1.02.1534 1.995.5325 2.8508 1.1098.4787-.457 0-.827-.283-1.2186.8958.3445 1.7108.8706 2.3937 1.545.1832-.1414.3007-.3514.3253-.5815a.849.849 0 0 0-.1947-.6372c.7474.4107 1.4.9742 1.915 1.654.3363-.3.4577-.7582.3046-1.175a19.014 19.014 0 0 1 2.1762 2.1761 1.827 1.827 0 0 0 .348-.9575c2.0456 1.9803 4.94 6.9854.74 8.9657a39.432 39.432 0 0 0-12.6433-6.6808zm32.2067 0a41.064 41.064 0 0 0-11.5335 7.8123c1 3.9605 6.1585 4.1564 8.0517 4.0476a1.415 1.415 0 0 1-.7399-.7399c1.1263-.145 2.2396-.3777 3.3295-.6964a1.11 1.11 0 0 1-.8704-.5223 9.551 9.551 0 0 0 3.003-1.2403c-.4246.0706-.8602-.0228-1.2186-.2612 1.0394-.4516 2.007-1.0527 2.8725-1.7844-.4166.0537-.8398-.0143-1.2186-.196.8494-.5174 1.6253-1.147 2.3067-1.8714a1.697 1.697 0 0 1-1.1969-.1089 9.228 9.228 0 0 0 1.9803-2.1761 1.458 1.458 0 0 1-1.371 0 11.752 11.752 0 0 0 1.8062-2.177 2.895 2.895 0 0 1-1.2839 0 6.068 6.068 0 0 1 1.1533-2.176 20.488 20.488 0 0 1-2.9378 0l.74-.7617a5.876 5.876 0 0 0-3.2424.3264c-.3917-.3046 0-.6963.4787-1.088a11.79 11.79 0 0 0-2.7201.6746c-.5223-.4788.1958-.827.5658-1.2622-1.02.1534-1.995.5325-2.8508 1.1098-.4787-.457 0-.827.283-1.2186-.8958.3445-1.7108.8706-2.3937 1.545a.849.849 0 0 1-.1523-1.2188c-.7474.4107-1.4.9742-1.915 1.654-.3364-.3-.4578-.7582-.3047-1.175-.783.6654-1.5107 1.393-2.176 2.176a1.828 1.828 0 0 1-.3482-.9575c-2.0455 1.9803-4.9398 6.9854-.74 8.9657 3.745-2.9704 8-5.2342 12.556-6.6807z"
fill="#75a928"/>
<path d="M33.494 47.4397c-.1418 3.8536-3.3706 6.8684-7.2248 6.746-3.8632.1722-7.1355-2.8177-7.3118-6.6807.1418-3.8536 3.3705-6.8684 7.2248-6.746 3.863-.1722 7.1355 2.8177 7.3118 6.6807zm-11.5-19.15c3.072 2.4345 3.6152 6.8867 1.2187 9.9884-1.9194 3.4344-6.234 4.7016-9.7056 2.8508-3.072-2.4346-3.6152-6.8867-1.2186-9.9885 1.9193-3.4343 6.2338-4.7016 9.7055-2.8507zm8.1488-.37c-3.072 2.4345-3.6152 6.8867-1.2186 9.9884 1.9193 3.4344 6.2338 4.7017 9.7055 2.8508 3.072-2.4346 3.6152-6.8867 1.2186-9.9885-1.9193-3.4343-6.2338-4.7016-9.7055-2.8507zM6.9452 31.554c3.2642-.8704 1.088 13.4703-1.545 12.295a7.747 7.747 0 0 1 1.545-12.2951zm37.6035-.2177c-3.264-.8704-1.088 13.4703 1.545 12.295 1.7518-1.6948 2.6038-4.1152 2.2998-6.5336a7.747 7.747 0 0 0-3.8449-5.7615zM33.494 20.6733c5.6144-.9575 10.293 2.3937 10.0972 8.487-.174 2.3938-12.0993-8.1605-10.0972-8.487zm-15.5158-.196C12.3638 19.52 7.685 22.87 7.88 28.9643c.174 2.3285 12.1646-8.1387 10.0973-8.487zm8.0734-1.4144c-3.3512 0-6.5284 2.4808-6.5284 3.9823s2.655 3.6777 6.5284 3.7212 6.5284-1.4798 6.5284-3.3512-3.6777-4.3523-6.6372-4.3523zm.196 37.212c2.916-.1306 6.833.9357 6.8548 2.3502s-3.547 4.4828-7.0507 4.3522-7.1594-2.9595-7.116-4.0476c0-1.5885 4.3522-2.83 7.3118-2.742zM15.454 47.875c2.176 2.5025 3.0248 6.92 1.284 8.204s-5.6362.5875-8.465-3.4818c-1.8933-3.4383-1.654-6.8984-.3047-7.9864 2.002-1.2187 5.114.4352 7.5077 3.2zm21.1737-.8052c-2.1762 2.633-3.5036 7.4423-1.8715 8.9874 1.5668 1.1968 5.7885 1.0445 8.9004-3.286a8.27 8.27 0 0 0 .2176-9.03c-1.915-1.4798-4.6787.4134-7.2465 3.3295z"
fill="#bc1142"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

260
auth/auth.go Normal file
View file

@ -0,0 +1,260 @@
package auth
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"strings"
_ "github.com/mattn/go-sqlite3"
"github.com/gmemstr/nas/common"
)
const (
enc = "cookie_session_encryption"
// This is the key with which each cookie is encrypted, I'll recommend moving it to a env file
cookieName = "NAS_SESSION"
cookieExpiry = 60 * 60 * 24 * 30 // 30 days in seconds
)
func UserPermissions(username string, permission int) (bool, error) {
db, err := sql.Open("sqlite3", "assets/config/users.db")
defer db.Close()
isAllowed := false
if err != nil {
return isAllowed, err
}
statement, err := db.Prepare("SELECT permissions FROM users WHERE username=?")
if err != nil {
return isAllowed, err
}
rows, err := statement.Query(username)
if err != nil {
return isAllowed, err
}
var level int
for rows.Next() {
err = rows.Scan(&level)
if err != nil {
return isAllowed, err
}
if level >= permission {
isAllowed = true
}
}
return isAllowed, nil
}
func RequireAuthorization(permission int) common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
usr, err := DecryptCookie(r)
if err != nil {
fmt.Println(err.Error())
if strings.Contains(r.Header.Get("Accept"), "html") || r.Method == "GET" {
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
return &common.HTTPError{
Message: "Unauthorized! Redirecting to /login",
StatusCode: http.StatusTemporaryRedirect,
}
}
return &common.HTTPError{
Message: "Unauthorized!",
StatusCode: http.StatusUnauthorized,
}
}
rc.User = usr
username := rc.User.Username
hasPermission, err := UserPermissions(string(username), permission)
if !hasPermission {
return &common.HTTPError{
Message: "Unauthorized! Redirecting to /admin",
StatusCode: http.StatusUnauthorized,
}
}
return nil
}
}
func CreateSession(u *common.User) (*http.Cookie, error) {
secret := os.Getenv("POGO_SECRET")
iv, err := generateRandomString(16)
if err != nil {
return nil, err
}
userJSON, err := json.Marshal(u)
if err != nil {
return nil, err
}
hexedJSON := hex.EncodeToString(userJSON)
encKey := deriveKey(enc, secret)
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
// Fill the block with 0x0e
if remBytes := len(hexedJSON) % aes.BlockSize; remBytes != 0 {
t := []byte(hexedJSON)
for i := 0; i < aes.BlockSize-remBytes; i++ {
t = append(t, 0x0e)
}
hexedJSON = string(t)
}
mode := cipher.NewCBCEncrypter(block, iv)
encCipher := make([]byte, len(hexedJSON)+aes.BlockSize)
mode.CryptBlocks(encCipher, []byte(hexedJSON))
cipherbase64 := base64urlencode(encCipher)
ivbase64 := base64urlencode(iv)
// Cookie format: iv.cipher.created_on.expire_on.HMAC
cookieStr := fmt.Sprintf("%s.%s", ivbase64, cipherbase64)
c := &http.Cookie{
Name: cookieName,
Value: cookieStr,
MaxAge: cookieExpiry,
}
return c, nil
}
func DecryptCookie(r *http.Request) (*common.User, error) {
secret := os.Getenv("POGO_SECRET")
c, err := r.Cookie(cookieName)
if err != nil {
if err != http.ErrNoCookie {
log.Printf("error in reading Cookie: %v", err)
}
return nil, err
}
csplit := strings.Split(c.Value, ".")
if len(csplit) != 2 {
return nil, errors.New("Invalid number of values in cookie")
}
ivb, cipherb := csplit[0], csplit[1]
iv, err := base64urldecode(ivb)
if err != nil {
return nil, err
}
dcipher, err := base64urldecode(cipherb)
if err != nil {
return nil, err
}
if len(iv) != 16 {
return nil, errors.New("IV length is not 16")
}
encKey := deriveKey(enc, secret)
if len(dcipher)%aes.BlockSize != 0 {
return nil, errors.New("ciphertext not multiple of blocksize")
}
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
buf := make([]byte, len(dcipher))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(buf, []byte(dcipher))
tstr := fmt.Sprintf("%x", buf)
// Remove aes padding, 0e is used because it was used in encryption to mark padding
padIndex := strings.Index(tstr, "0e")
if padIndex == -1 {
return nil, errors.New("Padding Index is -1")
}
tstr = tstr[:padIndex]
data, err := hex.DecodeString(tstr)
if err != nil {
return nil, err
}
data, err = hex.DecodeString(string(data))
if err != nil {
return nil, err
}
u := &common.User{}
err = json.Unmarshal(data, u)
if err != nil {
return nil, err
}
return u, nil
}
func deriveKey(msg, secret string) []byte {
key := []byte(secret)
sha256hash := hmac.New(sha256.New, key)
sha256hash.Write([]byte(msg))
return sha256hash.Sum(nil)
}
func generateRandomString(l int) ([]byte, error) {
rBytes := make([]byte, l)
_, err := rand.Read(rBytes)
if err != nil {
return nil, err
}
return rBytes, nil
}
func base64urldecode(str string) ([]byte, error) {
base64str := strings.Replace(string(str), "-", "+", -1)
base64str = strings.Replace(base64str, "_", "/", -1)
s, err := base64.RawStdEncoding.DecodeString(base64str)
if err != nil {
return nil, err
}
return s, nil
}
func base64urlencode(str []byte) string {
base64str := strings.Replace(string(str), "+", "-", -1)
base64str = strings.Replace(base64str, "/", "_", -1)
return base64.RawStdEncoding.EncodeToString([]byte(base64str))
}

View file

@ -1,10 +1,9 @@
package files
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/gmemstr/nas/auth"
"github.com/gmemstr/nas/common"
"github.com/gorilla/mux"
"io"
@ -32,43 +31,64 @@ type FileInfo struct {
Name string
}
func GetUserDirectory(r *http.Request, tier string) (string, string, string) {
usr, err := auth.DecryptCookie(r)
if err != nil {
return "", "", ""
}
username := usr.Username
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
panic(err)
}
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage := config.HotStorage + username
prefix := "files"
singleprefix := "file"
if tier == "cold" {
storage = config.ColdStorage + username
prefix = "archive"
singleprefix = "archived"
}
return storage, prefix, singleprefix
}
// Lists out directory using template.
func Listing(tier string) common.Handler {
func Listing() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
vars := mux.Vars(r)
id := vars["file"]
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
panic(err)
}
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage := config.HotStorage
prefix := "files"
singleprefix := "file"
if tier == "cold" {
storage = config.ColdStorage
prefix = "archive"
singleprefix = "archived"
tier := vars["tier"]
storage, prefix, singleprefix := GetUserDirectory(r, tier)
if storage == "" && prefix == "" && singleprefix == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return &common.HTTPError{
Message: "Unauthorized, or unable to find cookie",
StatusCode: http.StatusTemporaryRedirect,
}
}
path := storage
if id != "" {
path = path + id
}
if err != nil {
panic(err)
path = storage + id
}
fileDir, err := ioutil.ReadDir(path)
var fileList []FileInfo;
if err != nil && path == "" {
fmt.Println(path)
_ = os.MkdirAll(path, 0644)
}
var fileList []FileInfo
for _, file := range fileDir {
info := FileInfo{
@ -100,11 +120,12 @@ func Listing(tier string) common.Handler {
}
// Lists out directory using template.
func ViewFile(tier string) common.Handler {
func ViewFile() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
vars := mux.Vars(r)
id := vars["file"]
tier := vars["tier"]
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
@ -117,14 +138,8 @@ func ViewFile(tier string) common.Handler {
panic(err)
}
// Default to hot storage
storage := config.HotStorage
if tier == "cold" {
storage = config.ColdStorage
}
storage, _, _ := GetUserDirectory(r, tier)
path := storage + id
if err != nil {
panic(err)
}
common.ReadAndServeFile(path, w)
return nil
@ -166,50 +181,3 @@ func UploadFile() common.Handler {
}
}
func Md5File(tier string) common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
vars := mux.Vars(r)
id := vars["file"]
var returnMD5String string
d, err := ioutil.ReadFile("assets/config/config.json")
var config Config;
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage := config.HotStorage
if err != nil {
fmt.Println(err)
return nil
}
file, err := os.Open(storage + "/" + id)
if err != nil {
panic(err)
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
panic(err)
}
//Get the 16 bytes hash
hashInBytes := hash.Sum(nil)[:16]
//Convert the bytes to a string
returnMD5String = hex.EncodeToString(hashInBytes)
w.Write([]byte(returnMD5String))
return nil
}
}

23
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

68
frontend/README.md Normal file
View file

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

32
frontend/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"react-scripts": "3.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "rm -rf ../assets/web/* && react-scripts build && mv build/* ../assets/web",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

72
frontend/src/App.css Normal file
View file

@ -0,0 +1,72 @@
body {
font-family: Open Sans, Arial, sans-serif;
color: #454545;
font-size: 16px;
background-color: #fefefe;
}
.App {
margin: 2em auto;
max-width: 800px;
padding: 1em;
line-height: 1.4;
text-align: justify;
}
.Usages {
display: flex;
flex-direction: column;
}
.Navigation {
display: flex;
justify-content: space-around;
font-size: 32px;
}
.Navigation a {
color: #454545;
text-decoration: none;
}
/* From https://loading.io/css */
.LoadingSpinner {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.LoadingSpinner div {
box-sizing: border-box;
display: block;
position: absolute;
width: 51px;
height: 51px;
margin: 6px;
border: 6px solid #cef;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #cef transparent transparent transparent;
}
.LoadingSpinner div:nth-child(1) {
animation-delay: -0.45s;
}
.LoadingSpinner div:nth-child(2) {
animation-delay: -0.3s;
}
.LoadingSpinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

141
frontend/src/App.js Normal file
View file

@ -0,0 +1,141 @@
import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom'
import './App.css';
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Route exact path="/" component={Homepage} />
<Route exact path="/login" component={Login} />
<Route exact path="/hot" component={HotFileListing} />
</div>
</BrowserRouter>
);
}
}
class Login extends Component {
render() {
return (
<div className="LoginForm">
<form>
<label>Username <input type="text" name="username"></input></label>
<label>Password <input type="password" name="password"></input></label>
<input type="submit" value="Login"></input>
</form>
</div>
)
}
}
class Homepage extends Component {
constructor(props) {
super(props);
this.state = {
diskusage: {},
loading: true,
};
};
componentDidMount() {
fetch("/api/diskusage")
.then(response => response.json())
.then(data => this.setState({ diskusage: {
hot: 100 - Math.floor((data.HotStorage.Free) / (data.HotStorage.Total) * 100),
cold: 100 - Math.floor((data.ColdStorage.Free) / (data.ColdStorage.Total) * 100),
}, loading: false }));
}
render() {
const { loading } = this.state;
if (loading) {
return <div className="LoadingSpinner"><div></div><div></div><div></div><div></div></div>;
}
return (
<div>
<div className="Usages">
<span>Hot Storage Usage: {this.state.diskusage.hot}%</span>
<span>Cold Storage Usage: {this.state.diskusage.cold}%</span>
</div>
<div className="Navigation">
<Link to="/hot">Hot</Link>
<Link to="/cold">Cold</Link>
</div>
</div>
)
}
}
class HotFileListing extends Component {
constructor(props) {
super(props);
this.state = {
files: [],
loading: true,
};
};
componentDidMount() {
fetch("/api/hot/")
.then(response => response.json())
.then(data => this.setState({ files: data, loading: false }));
}
render() {
const { loading } = this.state;
if (loading) {
return <div className="LoadingSpinner"><div></div><div></div><div></div><div></div></div>;
}
return (
<div>
<FileList files={this.state.files.Files} />
</div>
)
}
}
class FileList extends Component {
render () {
let fileComponents = this.props.files.map((file) => {
if (file.IsDirectory) {
return <Directory dir={file}/>
}
return <File file={file}/>
})
return (
<div>
<ul>{fileComponents}</ul>
</div>
)
}
}
class Directory extends Component {
render() {
return (
<div>
<p>{this.props.dir.Name}/</p>
</div>
)
}
}
class File extends Component {
render() {
return (
<div>
<p>{this.props.file.Name}</p>
</div>
)
}
}
export default App;

9
frontend/src/App.test.js Normal file
View file

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

13
frontend/src/index.css Normal file
View file

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

12
frontend/src/index.js Normal file
View file

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

7
frontend/src/logo.svg Normal file
View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

10112
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,10 @@
package router
import (
"database/sql"
"fmt"
"github.com/gmemstr/nas/auth"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
@ -11,15 +14,6 @@ import (
"github.com/gorilla/mux"
)
type NewConfig struct {
Name string
Host string
Email string
Description string
Image string
PodcastURL string
}
func Handle(handlers ...common.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -43,61 +37,129 @@ func Init() *mux.Router {
r := mux.NewRouter()
// "Static" paths
r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/web/static"))))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("assets/web/static"))))
// Paths that require specific handlers
r.Handle("/", Handle(
auth.RequireAuthorization(1),
rootHandler(),
)).Methods("GET")
r.Handle(`/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/files/`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/archive/`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/login`, Handle(
loginHandler(),
)).Methods("POST", "GET")
r.Handle("/api/diskusage", Handle(
auth.RequireAuthorization(1),
system.DiskUsages(),
)).Methods("GET")
r.Handle("/api/files/", Handle(
files.Listing("hot"),
)).Methods("GET")
r.Handle(`/api/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Listing("hot"),
)).Methods("GET")
r.Handle(`/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("hot"),
r.Handle(`/api/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
auth.RequireAuthorization(1),
files.ViewFile(),
)).Methods("GET")
r.Handle("/api/upload", Handle(
files.UploadFile(),
)).Methods("POST")
r.Handle(`/api/filesmd5/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Md5File("hot"),
)).Methods("GET")
r.Handle("/api/archive/", Handle(
files.Listing("cold"),
r.Handle("/api/{tier:(?:hot|cold)}/", Handle(
auth.RequireAuthorization(1),
files.Listing(),
)).Methods("GET")
r.Handle(`/api/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Listing("cold"),
)).Methods("GET")
r.Handle(`/archived/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("cold"),
)).Methods("GET")
r.Handle(`/api/archivemd5/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Md5File("cold"),
r.Handle(`/api/{tier:^(?:hot|cold)$}/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
auth.RequireAuthorization(1),
files.Listing(),
)).Methods("GET")
return r
}
func loginHandler() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
if r.Method == "GET" {
w.Header().Set("Content-Type", "text/html")
file := "assets/web/index.html"
return common.ReadAndServeFile(file, w)
}
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
statement, err := db.Prepare("SELECT * FROM users WHERE username=?")
if _, err := auth.DecryptCookie(r); err == nil {
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
return nil
}
err = r.ParseForm()
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in parsing form: %v", err),
StatusCode: http.StatusBadRequest,
}
}
username := r.Form.Get("username")
password := r.Form.Get("password")
rows, err := statement.Query(username)
if username == "" || password == "" || err != nil {
return &common.HTTPError{
Message: "username or password is invalid",
StatusCode: http.StatusBadRequest,
}
}
var id int
var dbun string
var dbhsh string
var dbrn string
var dbem string
var dbperm int
for rows.Next() {
err := rows.Scan(&id, &dbun, &dbhsh, &dbrn, &dbem, &dbperm)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in decoding sql data", err),
StatusCode: http.StatusBadRequest,
}
}
}
// Create a cookie here because the credentials are correct
if bcrypt.CompareHashAndPassword([]byte(dbhsh), []byte(password)) == nil {
c, err := auth.CreateSession(&common.User{
Username: username,
})
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusInternalServerError,
}
}
// r.AddCookie(c)
w.Header().Add("Set-Cookie", c.String())
// And now redirect the user to admin page
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
db.Close()
return nil
}
return &common.HTTPError{
Message: "Invalid credentials!",
StatusCode: http.StatusUnauthorized,
}
}
}
// Handles /.
func rootHandler() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
@ -115,15 +177,6 @@ func rootHandler() common.Handler {
}
}
return common.ReadAndServeFile(file, w)
}
}
func fileList() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
w.Header().Set("Content-Type", "text/html")
file := "assets/web/listing.html"
return common.ReadAndServeFile(file, w)
}
}

View file

@ -3,8 +3,10 @@ package system
import (
"encoding/json"
"github.com/gmemstr/nas/common"
"github.com/gmemstr/nas/files"
"io/ioutil"
"net/http"
"os"
"syscall"
)
@ -41,20 +43,20 @@ func DiskUsages() common.Handler {
panic(err)
}
// Default to hot storage
storage := config.HotStorage
storage, _, _ := files.GetUserDirectory(r,"hot")
err = syscall.Statfs(storage, &statHot)
if err != nil {
panic(err)
_ = os.MkdirAll(storage, 0644)
}
hotStats := UsageStat{
Free: statHot.Bsize * int64(statHot.Bfree),
Free: statHot.Bsize * int64(statHot.Bfree),
Total: statHot.Bsize * int64(statHot.Blocks),
}
storage = config.ColdStorage
storage, _, _ = files.GetUserDirectory(r,"cold")
err = syscall.Statfs(storage, &statCold)
if err != nil {
panic(err)
_ = os.MkdirAll(storage, 0644)
}
coldStats := UsageStat{
Free: statCold.Bsize * int64(statCold.Bfree),

View file

@ -7,18 +7,86 @@
package main
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
"os"
"github.com/gmemstr/nas/router"
)
// Main function that defines routes
func main() {
// Define routes
// We're live
if _, err := os.Stat(".lock"); os.IsNotExist(err) {
createDatabase()
createLockFile()
}
r := router.Init()
fmt.Println("Your NAS instance is live on port :3000")
log.Fatal(http.ListenAndServe(":3000", r))
}
func createDatabase() {
fmt.Println("Initializing the database")
os.Create("assets/config/users.db")
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
fmt.Println("Problem opening database file! %v", err)
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS `users` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, `username` TEXT UNIQUE, `hash` TEXT, `realname` TEXT, `email` TEXT, `permissions` INTEGER )")
if err != nil {
fmt.Println("Problem creating database! %v", err)
}
text, err := GenerateRandomString(12)
if err != nil {
fmt.Println("Error randomly generating password", err)
}
fmt.Println("Admin password: ", text)
hash, err := bcrypt.GenerateFromPassword([]byte(text), 4)
if err != nil {
fmt.Println("Error generating hash", err)
}
if bcrypt.CompareHashAndPassword(hash, []byte(text)) == nil {
fmt.Println("Password hashed")
}
_, err = db.Exec("INSERT INTO users(id,username,hash,realname,email,permissions) VALUES (0,'admin','" + string(hash) + "','Administrator','admin@localhost',2)")
if err != nil {
fmt.Println("Problem creating database! %v", err)
}
defer db.Close()
}
func createLockFile() {
lock, err := os.Create(".lock")
if err != nil {
fmt.Println("Error: %v", err)
}
lock.Write([]byte("This file left intentionally empty"))
defer lock.Close()
}
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
func GenerateRandomString(s int) (string, error) {
b, err := GenerateRandomBytes(s)
return base64.URLEncoding.EncodeToString(b), err
}