Web client refinement (#16)
* fix(files/handler): add base64 decode for content * fix(singleuser): pick user name from jwt token and encode content * fix(singleuser): add public path check, abstract user info from token * fix(singleuser): update singleuser client * fix(server): fix test and enable auth by default * feat(client/web): add web client * fix(client/web): refine css styles * fix(client/web): refine styles * fix(client/web): refine styles, add test and fix bugs * test(client/web): add web client tests * fix(client/web): refactor client interface and enhance the robustness * chore(client/web): ignore js bundles * test(files): call sync before check Co-authored-by: Jia He <jiah@nvidia.com>
This commit is contained in:
parent
0265baf1b1
commit
ba6a5373d1
53 changed files with 9192 additions and 158 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,3 +6,5 @@
|
||||||
**/dist
|
**/dist
|
||||||
**/vendor
|
**/vendor
|
||||||
**/yarn-error
|
**/yarn-error
|
||||||
|
**/public/static/*/*.js
|
||||||
|
**/public/static/**/*.js
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<script src="dist/assets.bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<script src="dist/api_test.bundle.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
BIN
public/ggb.jpg
BIN
public/ggb.jpg
Binary file not shown.
Before Width: | Height: | Size: 630 KiB |
|
@ -1,16 +1,94 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="utf-8" />
|
<title>Quickshare</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
<meta
|
||||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
name="viewport"
|
||||||
<script src="dist/assets.bundle.js"></script>
|
content="initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no,uc-fitscreen=yes"
|
||||||
</head>
|
/>
|
||||||
|
<meta class="chrome-color" name="theme-color" content="black" />
|
||||||
<body>
|
<script src="/static/js/react.development.js?v=16.8.6"></script>
|
||||||
<div id="app"></div>
|
<script src="/static/js/react-dom.development.js?v=16.8.6"></script>
|
||||||
<script src="dist/admin.bundle.js"></script>
|
<script src="/static/js/immutable.min.js?v=4.0.0-rc.12"></script>
|
||||||
</body>
|
<!-- <link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="57x57"
|
||||||
|
href="/static/fav/apple-icon-57x57.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="60x60"
|
||||||
|
href="/static/fav/apple-icon-60x60.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="72x72"
|
||||||
|
href="/static/fav/apple-icon-72x72.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="76x76"
|
||||||
|
href="/static/fav/apple-icon-76x76.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="114x114"
|
||||||
|
href="/static/fav/apple-icon-114x114.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="120x120"
|
||||||
|
href="/static/fav/apple-icon-120x120.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="144x144"
|
||||||
|
href="/static/fav/apple-icon-144x144.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="152x152"
|
||||||
|
href="/static/fav/apple-icon-152x152.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/static/fav/apple-icon-180x180.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/static/fav/android-icon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/static/fav/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="96x96"
|
||||||
|
href="/static/fav/favicon-96x96.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/static/fav/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/static/fav/manifest.json" />
|
||||||
|
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||||
|
<meta
|
||||||
|
name="msapplication-TileImage"
|
||||||
|
content="/static/fav/ms-icon-144x144.png"
|
||||||
|
/> -->
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content"><div id="mount"></div></div>
|
||||||
|
<script src="static/default-vendors-node_modules_axios_index_js-node_modules_css-loader_dist_runtime_api_js-node_-e9ca3b.bundle.js?910792307d026e16bec7"></script><script src="static/main.bundle.js?910792307d026e16bec7"></script></body>
|
||||||
</html>
|
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
BIN
public/static/img/prism.png
Normal file
BIN
public/static/img/prism.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
public/static/img/textured_paper.png
Normal file
BIN
public/static/img/textured_paper.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
|
@ -1,68 +0,0 @@
|
||||||
html {
|
|
||||||
background: url("ggb.jpg") no-repeat left center fixed;
|
|
||||||
background-size: auto 100%;
|
|
||||||
/* background-image: url("squared_metal.png"); repeat fixed*/
|
|
||||||
background-color: black;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
color: #333;
|
|
||||||
font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anm-rotate {
|
|
||||||
animation: rotate 1s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
20% {
|
|
||||||
transform: rotate(72deg);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: rotate(144deg);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: rotate(216deg);
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
transform: rotate(288deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.anm-lighter:hover {
|
|
||||||
animation: lighter 0.5 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lighter {
|
|
||||||
0% {
|
|
||||||
opacity: 100%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
background: #2ecc71;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
::selection {
|
|
||||||
background: #2ecc71;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
|
@ -34,19 +34,15 @@ func (cl *SingleUserClient) Login(user, pwd string) (*http.Response, string, []e
|
||||||
End()
|
End()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *SingleUserClient) Logout(user string, token *http.Cookie) (*http.Response, string, []error) {
|
func (cl *SingleUserClient) Logout(token *http.Cookie) (*http.Response, string, []error) {
|
||||||
return cl.r.Post(cl.url("/v1/users/logout")).
|
return cl.r.Post(cl.url("/v1/users/logout")).
|
||||||
Send(su.LogoutReq{
|
|
||||||
User: user,
|
|
||||||
}).
|
|
||||||
AddCookie(token).
|
AddCookie(token).
|
||||||
End()
|
End()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *SingleUserClient) SetPwd(user, oldPwd, newPwd string, token *http.Cookie) (*http.Response, string, []error) {
|
func (cl *SingleUserClient) SetPwd(oldPwd, newPwd string, token *http.Cookie) (*http.Response, string, []error) {
|
||||||
return cl.r.Patch(cl.url("/v1/users/pwd")).
|
return cl.r.Patch(cl.url("/v1/users/pwd")).
|
||||||
Send(su.SetPwdReq{
|
Send(su.SetPwdReq{
|
||||||
User: user,
|
|
||||||
OldPwd: oldPwd,
|
OldPwd: oldPwd,
|
||||||
NewPwd: newPwd,
|
NewPwd: newPwd,
|
||||||
}).
|
}).
|
||||||
|
|
8
src/client/web/.babelrc
Normal file
8
src/client/web/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"presets": ["@babel/env", "@babel/react"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["@babel/plugin-transform-runtime"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/client/web/.gitignore
vendored
Normal file
3
src/client/web/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
**/node_modules/**
|
||||||
|
**/yarn-error.log
|
||||||
|
**/bundle.js
|
94
src/client/web/build/template/index.template.dev.html
Normal file
94
src/client/web/build/template/index.template.dev.html
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Quickshare</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no,uc-fitscreen=yes"
|
||||||
|
/>
|
||||||
|
<meta class="chrome-color" name="theme-color" content="black" />
|
||||||
|
<script src="/static/js/react.development.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/react-dom.development.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/immutable.min.js?v=4.0.0-rc.12"></script>
|
||||||
|
<!-- <link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="57x57"
|
||||||
|
href="/static/fav/apple-icon-57x57.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="60x60"
|
||||||
|
href="/static/fav/apple-icon-60x60.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="72x72"
|
||||||
|
href="/static/fav/apple-icon-72x72.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="76x76"
|
||||||
|
href="/static/fav/apple-icon-76x76.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="114x114"
|
||||||
|
href="/static/fav/apple-icon-114x114.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="120x120"
|
||||||
|
href="/static/fav/apple-icon-120x120.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="144x144"
|
||||||
|
href="/static/fav/apple-icon-144x144.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="152x152"
|
||||||
|
href="/static/fav/apple-icon-152x152.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/static/fav/apple-icon-180x180.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/static/fav/android-icon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/static/fav/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="96x96"
|
||||||
|
href="/static/fav/favicon-96x96.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/static/fav/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/static/fav/manifest.json" />
|
||||||
|
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||||
|
<meta
|
||||||
|
name="msapplication-TileImage"
|
||||||
|
content="/static/fav/ms-icon-144x144.png"
|
||||||
|
/> -->
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content"><div id="mount"></div></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
94
src/client/web/build/template/index.template.html
Normal file
94
src/client/web/build/template/index.template.html
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Quickshare</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no,uc-fitscreen=yes"
|
||||||
|
/>
|
||||||
|
<meta class="chrome-color" name="theme-color" content="black" />
|
||||||
|
<!-- <script src="/static/js/react.production.min.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/react-dom.production.min.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/immutable.min.js?v=4.0.0-rc.12"></script>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="57x57"
|
||||||
|
href="/static/fav/apple-icon-57x57.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="60x60"
|
||||||
|
href="/static/fav/apple-icon-60x60.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="72x72"
|
||||||
|
href="/static/fav/apple-icon-72x72.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="76x76"
|
||||||
|
href="/static/fav/apple-icon-76x76.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="114x114"
|
||||||
|
href="/static/fav/apple-icon-114x114.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="120x120"
|
||||||
|
href="/static/fav/apple-icon-120x120.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="144x144"
|
||||||
|
href="/static/fav/apple-icon-144x144.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="152x152"
|
||||||
|
href="/static/fav/apple-icon-152x152.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/static/fav/apple-icon-180x180.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/static/fav/android-icon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/static/fav/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="96x96"
|
||||||
|
href="/static/fav/favicon-96x96.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/static/fav/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/static/fav/manifest.json" /> -->
|
||||||
|
<!-- <meta name="msapplication-TileColor" content="#ffffff" /> -->
|
||||||
|
<!-- <meta
|
||||||
|
name="msapplication-TileImage"
|
||||||
|
content="/static/fav/ms-icon-144x144.png"
|
||||||
|
/> -->
|
||||||
|
<!-- <meta name="theme-color" content="#ffffff" /> -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content"><div id="mount"></div></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
83
src/client/web/package.json
Normal file
83
src/client/web/package.json
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "web client for quickshare",
|
||||||
|
"main": "",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.app.prod.js",
|
||||||
|
"build:watch": "webpack --config webpack.app.prod.js --watch",
|
||||||
|
"build:dev": "webpack --config webpack.app.dev.js --watch",
|
||||||
|
"build:admin": "webpack --config webpack.admin.prod.js",
|
||||||
|
"build:admin:watch": "webpack --config webpack.admin.prod.js --watch",
|
||||||
|
"build:admin:dev": "webpack --config webpack.admin.dev.js --watch",
|
||||||
|
"build:task": "webpack --config webpack.task.prod.js",
|
||||||
|
"build:task:watch": "webpack --config webpack.task.prod.js --watch",
|
||||||
|
"build:task:dev": "webpack --config webpack.task.dev.js --watch",
|
||||||
|
"e2e": "jest -c jest.e2e.config.js",
|
||||||
|
"e2e:watch": "jest --watch -c jest.e2e.config.js",
|
||||||
|
"test": "jest test",
|
||||||
|
"test:watch": "jest test --watch",
|
||||||
|
"copy": "cp -r ../../static ../../../dockers/nginx/"
|
||||||
|
},
|
||||||
|
"author": "hexxa",
|
||||||
|
"license": "LGPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||||
|
"@babel/preset-env": "^7.4.4",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"@types/assert": "^1.4.2",
|
||||||
|
"@types/deep-diff": "^1.0.0",
|
||||||
|
"@types/jest": "^24.0.12",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"css-loader": "^2.1.1",
|
||||||
|
"deep-diff": "^1.0.2",
|
||||||
|
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||||
|
"jest": "^24.8.0",
|
||||||
|
"source-map-loader": "^0.2.4",
|
||||||
|
"style-loader": "^0.23.1",
|
||||||
|
"terser-webpack-plugin": "^1.3.0",
|
||||||
|
"ts-jest": "^24.0.2",
|
||||||
|
"ts-loader": "^6.0.0",
|
||||||
|
"ts-node": "^8.2.0",
|
||||||
|
"tslint": "^5.16.0",
|
||||||
|
"typescript": "^3.4.3",
|
||||||
|
"uglifyjs-webpack-plugin": "^2.1.3",
|
||||||
|
"webpack": "^5.0.0-rc.6",
|
||||||
|
"webpack-bundle-analyzer": "^3.3.2",
|
||||||
|
"webpack-cli": "^3.3.0",
|
||||||
|
"webpack-merge": "^4.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
|
"@types/immutable": "^3.8.7",
|
||||||
|
"@types/react": "^16.8.13",
|
||||||
|
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||||
|
"@types/react-dom": "^16.8.4",
|
||||||
|
"@types/react-svg": "^5.0.0",
|
||||||
|
"@types/throttle-debounce": "^1.1.1",
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"filesize": "^6.1.0",
|
||||||
|
"immutable": "^4.0.0-rc.12",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-svg": "^8.0.6",
|
||||||
|
"throttle-debounce": "^2.1.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testMatch": [
|
||||||
|
"**/src/**/__test__/**/*.test.ts",
|
||||||
|
"**/src/**/__test__/**/*.test.tsx"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"\\.(ts|tsx)$": "ts-jest"
|
||||||
|
},
|
||||||
|
"verbose": true,
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"autoBump": {}
|
||||||
|
}
|
14
src/client/web/src/app.tsx
Normal file
14
src/client/web/src/app.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import { StateMgr } from "./components/state_mgr";
|
||||||
|
|
||||||
|
import "./theme/reset.css";
|
||||||
|
import "./theme/white.css";
|
||||||
|
// TODO: it fails in jest preprocessor now
|
||||||
|
import "./theme/style.css";
|
||||||
|
import "./theme/desktop.css";
|
||||||
|
import "./theme/color.css";
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.render(<StateMgr />, document.getElementById("mount"));
|
104
src/client/web/src/client/files.ts
Normal file
104
src/client/web/src/client/files.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import {
|
||||||
|
BaseClient,
|
||||||
|
Response,
|
||||||
|
UploadStatusResp,
|
||||||
|
ListResp,
|
||||||
|
} from "./";
|
||||||
|
|
||||||
|
const filePathQuery = "fp";
|
||||||
|
const listDirQuery = "dp";
|
||||||
|
// TODO: get timeout from server
|
||||||
|
|
||||||
|
export class FilesClient extends BaseClient {
|
||||||
|
constructor(url: string) {
|
||||||
|
super(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
create = (filePath: string, fileSize: number): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "post",
|
||||||
|
url: `${this.url}/v1/fs/files`,
|
||||||
|
data: {
|
||||||
|
path: filePath,
|
||||||
|
fileSize: fileSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
delete = (filePath: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "delete",
|
||||||
|
url: `${this.url}/v1/fs/files`,
|
||||||
|
params: {
|
||||||
|
[filePathQuery]: filePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata = (filePath: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/fs/metadata`,
|
||||||
|
params: {
|
||||||
|
[filePathQuery]: filePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdir = (dirpath: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "post",
|
||||||
|
url: `${this.url}/v1/fs/dirs`,
|
||||||
|
data: {
|
||||||
|
path: dirpath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
move = (oldPath: string, newPath: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "patch",
|
||||||
|
url: `${this.url}/v1/fs/files/move`,
|
||||||
|
data: {
|
||||||
|
oldPath,
|
||||||
|
newPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadChunk = (
|
||||||
|
filePath: string,
|
||||||
|
content: string | ArrayBuffer,
|
||||||
|
offset: number
|
||||||
|
): Promise<Response<UploadStatusResp>> => {
|
||||||
|
return this.do({
|
||||||
|
method: "patch",
|
||||||
|
url: `${this.url}/v1/fs/files/chunks`,
|
||||||
|
data: {
|
||||||
|
path: filePath,
|
||||||
|
content,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadStatus = (filePath: string): Promise<Response<UploadStatusResp>> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/fs/files/chunks`,
|
||||||
|
params: {
|
||||||
|
[filePathQuery]: filePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
list = (dirPath: string): Promise<Response<ListResp>> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/fs/dirs`,
|
||||||
|
params: {
|
||||||
|
[listDirQuery]: dirPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
82
src/client/web/src/client/files_mock.ts
Normal file
82
src/client/web/src/client/files_mock.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import {MetadataResp, UploadStatusResp, ListResp} from "./";
|
||||||
|
|
||||||
|
export class FilesClient {
|
||||||
|
private url: string;
|
||||||
|
private createMockResp: number;
|
||||||
|
private deleteMockResp: number;
|
||||||
|
private metadataMockResp: MetadataResp | null;
|
||||||
|
private mkdirMockResp: number | null;
|
||||||
|
private moveMockResp: number;
|
||||||
|
private uploadChunkMockResp: UploadStatusResp | null;
|
||||||
|
private uploadStatusMockResp: UploadStatusResp | null;
|
||||||
|
private listMockResp: ListResp | null;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
createMock = (resp: number) => {
|
||||||
|
this.createMockResp = resp;
|
||||||
|
}
|
||||||
|
deleteMock = (resp: number) => {
|
||||||
|
this.deleteMockResp = resp;
|
||||||
|
}
|
||||||
|
metadataMock = (resp: MetadataResp | null) => {
|
||||||
|
this.metadataMockResp = resp;
|
||||||
|
}
|
||||||
|
mkdirMock = (resp: number | null) => {
|
||||||
|
this.mkdirMockResp = resp;
|
||||||
|
}
|
||||||
|
moveMock = (resp: number) => {
|
||||||
|
this.moveMockResp = resp;
|
||||||
|
}
|
||||||
|
uploadChunkMock = (resp: UploadStatusResp | null) => {
|
||||||
|
this.uploadChunkMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadStatusMock = (resp: UploadStatusResp | null) => {
|
||||||
|
this.uploadStatusMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
listMock = (resp: ListResp | null) => {
|
||||||
|
this.listMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(filePath: string, fileSize: number): Promise<number> {
|
||||||
|
return this.createMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(filePath: string): Promise<number> {
|
||||||
|
return this.deleteMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async metadata(filePath: string): Promise<MetadataResp | null> {
|
||||||
|
return this.metadataMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(dirpath: string): Promise<number | null> {
|
||||||
|
return this.mkdirMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(oldPath: string, newPath: string): Promise<number> {
|
||||||
|
return this.moveMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadChunk(
|
||||||
|
filePath: string,
|
||||||
|
content: string | ArrayBuffer,
|
||||||
|
offset: number
|
||||||
|
): Promise<UploadStatusResp | null> {
|
||||||
|
return this.uploadChunkMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadStatus(filePath: string): Promise<UploadStatusResp | null> {
|
||||||
|
return this.uploadStatusMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(dirPath: string): Promise<ListResp | null> {
|
||||||
|
return this.listMockResp;
|
||||||
|
}
|
||||||
|
}
|
110
src/client/web/src/client/index.ts
Normal file
110
src/client/web/src/client/index.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import axios, { AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
|
export const defaultTimeout = 10000;
|
||||||
|
|
||||||
|
export interface MetadataResp {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
modTime: string;
|
||||||
|
isDir: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadStatusResp {
|
||||||
|
path: string;
|
||||||
|
isDir: boolean;
|
||||||
|
fileSize: number;
|
||||||
|
uploaded: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListResp {
|
||||||
|
metadatas: MetadataResp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUsersClient {
|
||||||
|
login: (user: string, pwd: string) => Promise<Response>
|
||||||
|
logout: () => Promise<Response>
|
||||||
|
isAuthed: () => Promise<Response>
|
||||||
|
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilesClient {
|
||||||
|
create: (filePath: string, fileSize: number) => Promise<Response>;
|
||||||
|
delete: (filePath: string) => Promise<Response>;
|
||||||
|
metadata: (filePath: string) => Promise<Response>;
|
||||||
|
mkdir: (dirpath: string) => Promise<Response>;
|
||||||
|
move: (oldPath: string, newPath: string) => Promise<Response>;
|
||||||
|
uploadChunk: (
|
||||||
|
filePath: string,
|
||||||
|
content: string | ArrayBuffer,
|
||||||
|
offset: number
|
||||||
|
) => Promise<Response<UploadStatusResp>>;
|
||||||
|
uploadStatus: (filePath: string) => Promise<Response<UploadStatusResp>>;
|
||||||
|
list: (dirPath: string) => Promise<Response<ListResp>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response<T = any> {
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TimeoutResp: Response<any> = {
|
||||||
|
status: 408,
|
||||||
|
data: {},
|
||||||
|
statusText: "Request Timeout",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6xx are custom errors for expressing errors which can not be expressed by http status code
|
||||||
|
export const EmptyBodyResp: Response<any> = {
|
||||||
|
status: 601,
|
||||||
|
data: {},
|
||||||
|
statusText: "Empty Response Body",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnknownErrResp = (errMsg: string): Response<any> => {
|
||||||
|
return {
|
||||||
|
status: 600,
|
||||||
|
data: {},
|
||||||
|
statusText: errMsg,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseClient {
|
||||||
|
protected url: string;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async do(config: AxiosRequestConfig): Promise<Response> {
|
||||||
|
let returned = false;
|
||||||
|
const src = axios.CancelToken.source();
|
||||||
|
|
||||||
|
return new Promise((resolve: (ret: Response) => void) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!returned) {
|
||||||
|
src.cancel("request timeout");
|
||||||
|
// resolve(TimeoutResp);
|
||||||
|
}
|
||||||
|
}, defaultTimeout);
|
||||||
|
|
||||||
|
axios({ ...config, cancelToken: src.token })
|
||||||
|
.then((resp) => {
|
||||||
|
returned = true;
|
||||||
|
resolve(resp);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
const errMsg = e.toString();
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
if (errMsg.includes("i/o timeput")) {
|
||||||
|
resolve(TimeoutResp);
|
||||||
|
} else if (errMsg.includes("ERR_EMPTY")) {
|
||||||
|
resolve(EmptyBodyResp);
|
||||||
|
} else {
|
||||||
|
resolve(UnknownErrResp(errMsg));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
109
src/client/web/src/client/uploader.ts
Normal file
109
src/client/web/src/client/uploader.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { List } from "immutable";
|
||||||
|
|
||||||
|
import { Response, UnknownErrResp, UploadStatusResp } from "./";
|
||||||
|
import { FilesClient } from "../client/files";
|
||||||
|
|
||||||
|
const defaultChunkLen = 1024 * 1024 * 30; // 15MB/s
|
||||||
|
const speedDownRatio = 0.5;
|
||||||
|
const speedUpRatio = 1.1;
|
||||||
|
|
||||||
|
export class FileUploader {
|
||||||
|
private reader = new FileReader();
|
||||||
|
private client = new FilesClient("");
|
||||||
|
private file: File;
|
||||||
|
private filePath: string;
|
||||||
|
private offset: number;
|
||||||
|
private chunkLen: number = defaultChunkLen;
|
||||||
|
private progressCb: (filePath: string, progress: number) => void;
|
||||||
|
private errMsg: string | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
file: File,
|
||||||
|
filePath: string,
|
||||||
|
progressCb: (filePath: string, progress: number) => void
|
||||||
|
) {
|
||||||
|
this.file = file;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.progressCb = progressCb;
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = (): string | null => {
|
||||||
|
return this.errMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
start = async (): Promise<boolean> => {
|
||||||
|
const resp = await this.client.create(this.filePath, this.file.size);
|
||||||
|
switch (resp.status) {
|
||||||
|
case 200:
|
||||||
|
return await this.upload();
|
||||||
|
default:
|
||||||
|
this.errMsg = `failed to create ${this.filePath}: status=${resp.statusText}`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
upload = async (): Promise<boolean> => {
|
||||||
|
while (
|
||||||
|
this.chunkLen > 0 &&
|
||||||
|
this.offset >= 0 &&
|
||||||
|
this.offset < this.file.size
|
||||||
|
) {
|
||||||
|
let uploadPromise = new Promise<Response<UploadStatusResp>>(
|
||||||
|
(resolve: (resp: Response<UploadStatusResp>) => void) => {
|
||||||
|
this.reader.onerror = (ev: ProgressEvent<FileReader>) => {
|
||||||
|
resolve(UnknownErrResp(this.reader.error.toString()));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => {
|
||||||
|
const dataURL = ev.target.result as string; // readAsDataURL
|
||||||
|
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1);
|
||||||
|
this.client
|
||||||
|
.uploadChunk(this.filePath, base64Chunk, this.offset)
|
||||||
|
.then((resp: Response<UploadStatusResp>) => {
|
||||||
|
resolve(resp);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
resolve(UnknownErrResp(e.toString()));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const chunkRightPos =
|
||||||
|
this.offset + this.chunkLen > this.file.size
|
||||||
|
? this.file.size
|
||||||
|
: this.offset + this.chunkLen;
|
||||||
|
const blob = this.file.slice(this.offset, chunkRightPos);
|
||||||
|
this.reader.readAsDataURL(blob);
|
||||||
|
|
||||||
|
const uploadResp = await uploadPromise;
|
||||||
|
if (uploadResp.status === 200 && uploadResp.data != null) {
|
||||||
|
this.offset = uploadResp.data.uploaded;
|
||||||
|
this.chunkLen = Math.ceil(this.chunkLen * speedUpRatio);
|
||||||
|
} else {
|
||||||
|
this.errMsg = uploadResp.statusText;
|
||||||
|
this.chunkLen = Math.ceil(this.chunkLen * speedDownRatio);
|
||||||
|
const uploadStatusResp = await this.client.uploadStatus(this.filePath);
|
||||||
|
|
||||||
|
console.log(uploadStatusResp.status);
|
||||||
|
|
||||||
|
if (uploadStatusResp.status === 200) {
|
||||||
|
this.offset = uploadStatusResp.data.uploaded;
|
||||||
|
} else if (uploadStatusResp.status === 600) {
|
||||||
|
this.errMsg = "unknown error";
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// do nothing and retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressCb(this.filePath, Math.ceil(this.offset / this.file.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunkLen === 0) {
|
||||||
|
this.errMsg = "network is bad, please retry later.";
|
||||||
|
}
|
||||||
|
return this.offset === this.file.size;
|
||||||
|
};
|
||||||
|
}
|
46
src/client/web/src/client/users.ts
Normal file
46
src/client/web/src/client/users.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { BaseClient, Response } from "./";
|
||||||
|
|
||||||
|
|
||||||
|
export class UsersClient extends BaseClient {
|
||||||
|
constructor(url: string) {
|
||||||
|
super(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
login = (user: string, pwd: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "post",
|
||||||
|
url: `${this.url}/v1/users/login`,
|
||||||
|
data: {
|
||||||
|
user,
|
||||||
|
pwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// token cookie is set by browser
|
||||||
|
logout = (): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "post",
|
||||||
|
url: `${this.url}/v1/users/logout`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthed = (): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/users/isauthed`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// token cookie is set by browser
|
||||||
|
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "patch",
|
||||||
|
url: `${this.url}/v1/users/pwd`,
|
||||||
|
data: {
|
||||||
|
oldPwd,
|
||||||
|
newPwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
45
src/client/web/src/client/users_mock.ts
Normal file
45
src/client/web/src/client/users_mock.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
// TODO: replace this with jest mocks
|
||||||
|
export class MockUsersClient {
|
||||||
|
private url: string;
|
||||||
|
private loginResp: number;
|
||||||
|
private logoutResp: number;
|
||||||
|
private isAuthedResp: number;
|
||||||
|
private setPwdResp: number;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLoginResp(status: number) {
|
||||||
|
this.loginResp = status;
|
||||||
|
}
|
||||||
|
mocklogoutResp(status: number) {
|
||||||
|
this.logoutResp = status;
|
||||||
|
}
|
||||||
|
mockisAuthedResp(status: number) {
|
||||||
|
this.isAuthedResp = status;
|
||||||
|
}
|
||||||
|
mocksetPwdResp(status: number) {
|
||||||
|
this.setPwdResp = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(user: string, pwd: string): Promise<number> {
|
||||||
|
return this.loginResp
|
||||||
|
}
|
||||||
|
|
||||||
|
// token cookie is set by browser
|
||||||
|
async logout(): Promise<number> {
|
||||||
|
return this.logoutResp
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAuthed(): Promise<number> {
|
||||||
|
return this.isAuthedResp
|
||||||
|
}
|
||||||
|
|
||||||
|
// token cookie is set by browser
|
||||||
|
async setPwd(oldPwd: string, newPwd: string): Promise<number> {
|
||||||
|
return this.setPwdResp
|
||||||
|
}
|
||||||
|
}
|
7
src/client/web/src/common.ts
Normal file
7
src/client/web/src/common.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const range = (start: number, end: number): Array<number> => {
|
||||||
|
let array = new Array(0);
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
array.push(i);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
32
src/client/web/src/components/__test__/auth_pane.test.tsx
Normal file
32
src/client/web/src/components/__test__/auth_pane.test.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { init } from "../core_state";
|
||||||
|
import { Updater } from "../auth_pane";
|
||||||
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
|
|
||||||
|
describe("AuthPane", () => {
|
||||||
|
test("Updater: initIsAuthed", () => {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
loginStatus: 200,
|
||||||
|
isAuthed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loginStatus: 500,
|
||||||
|
isAuthed: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const client = new MockUsersClient("foobarurl");
|
||||||
|
Updater.setClient(client);
|
||||||
|
const coreState = init();
|
||||||
|
|
||||||
|
tests.forEach(async (tc) => {
|
||||||
|
client.mockisAuthedResp(tc.loginStatus);
|
||||||
|
await Updater.initIsAuthed().then(() => {
|
||||||
|
const newState = Updater.setAuthPane(coreState);
|
||||||
|
expect(newState.panel.authPane.authed).toEqual(tc.isAuthed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
10
src/client/web/src/components/__test__/browser.test.tsx
Normal file
10
src/client/web/src/components/__test__/browser.test.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { init } from "../core_state";
|
||||||
|
import { Updater } from "../browser";
|
||||||
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
|
|
||||||
|
describe("Browser", () => {
|
||||||
|
test("Updater: ", () => {
|
||||||
|
});
|
||||||
|
});
|
151
src/client/web/src/components/auth_pane.tsx
Normal file
151
src/client/web/src/components/auth_pane.tsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ICoreState } from "./core_state";
|
||||||
|
import { IUsersClient } from "../client";
|
||||||
|
import { UsersClient } from "../client/users";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
authed: boolean;
|
||||||
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Updater {
|
||||||
|
private static props: Props;
|
||||||
|
private static client: IUsersClient;
|
||||||
|
|
||||||
|
static init = (props: Props) => (Updater.props = { ...props });
|
||||||
|
|
||||||
|
static setClient = (client: IUsersClient): void => {
|
||||||
|
Updater.client = client;
|
||||||
|
};
|
||||||
|
|
||||||
|
static login = async (user: string, pwd: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.login(user, pwd);
|
||||||
|
Updater.setAuthed(resp.status === 200);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static logout = async (): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.logout();
|
||||||
|
Updater.setAuthed(false);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static isAuthed = async (): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.isAuthed();
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static initIsAuthed = async (): Promise<void> => {
|
||||||
|
return Updater.isAuthed().then((isAuthed) => {
|
||||||
|
Updater.setAuthed(isAuthed);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
static setAuthed = (isAuthed: boolean) => {
|
||||||
|
Updater.props.authed = isAuthed;
|
||||||
|
};
|
||||||
|
|
||||||
|
static setAuthPane = (preState: ICoreState): ICoreState => {
|
||||||
|
preState.panel.authPane = {
|
||||||
|
...preState.panel.authPane,
|
||||||
|
...Updater.props,
|
||||||
|
};
|
||||||
|
return preState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
user: string;
|
||||||
|
pwd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthPane extends React.Component<Props, State, {}> {
|
||||||
|
private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
constructor(p: Props) {
|
||||||
|
super(p);
|
||||||
|
Updater.init(p);
|
||||||
|
Updater.setClient(new UsersClient(""));
|
||||||
|
this.update = p.update;
|
||||||
|
this.state = {
|
||||||
|
user: "",
|
||||||
|
pwd: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
this.initIsAuthed();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUser = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ user: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
changePwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ pwd: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
initIsAuthed = () => {
|
||||||
|
Updater.initIsAuthed().then(() => {
|
||||||
|
this.update(Updater.setAuthPane);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
login = () => {
|
||||||
|
Updater.login(this.state.user, this.state.pwd).then((ok: boolean) => {
|
||||||
|
if (ok) {
|
||||||
|
this.update(Updater.setAuthPane);
|
||||||
|
this.setState({ user: "", pwd: "" });
|
||||||
|
} else {
|
||||||
|
alert("Failed to login.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logout = () => {
|
||||||
|
Updater.logout().then((ok: boolean) => {
|
||||||
|
if (ok) {
|
||||||
|
this.update(Updater.setAuthPane);
|
||||||
|
this.setState({ user: "", pwd: "" });
|
||||||
|
} else {
|
||||||
|
alert("Failed to login.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ display: this.props.authed ? "none" : "inherit" }}>
|
||||||
|
<input
|
||||||
|
name="user"
|
||||||
|
type="text"
|
||||||
|
onChange={this.changeUser}
|
||||||
|
value={this.state.user}
|
||||||
|
className="margin-r-m black0-font"
|
||||||
|
style={{ width: "6rem" }}
|
||||||
|
placeholder="user name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="pwd"
|
||||||
|
type="password"
|
||||||
|
onChange={this.changePwd}
|
||||||
|
value={this.state.pwd}
|
||||||
|
className="margin-r-m black0-font"
|
||||||
|
style={{ width: "6rem" }}
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<button onClick={this.login} className="green0-bg white-font">
|
||||||
|
Log in
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
||||||
|
<button
|
||||||
|
onClick={this.logout}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
549
src/client/web/src/components/browser.tsx
Normal file
549
src/client/web/src/components/browser.tsx
Normal file
|
@ -0,0 +1,549 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { List, Map } from "immutable";
|
||||||
|
import * as Filesize from "filesize";
|
||||||
|
|
||||||
|
import { ICoreState } from "./core_state";
|
||||||
|
import { IUsersClient, IFilesClient, MetadataResp } from "../client";
|
||||||
|
import { FilesClient } from "../client/files";
|
||||||
|
import { UsersClient } from "../client/users";
|
||||||
|
import { FileUploader } from "../client/uploader";
|
||||||
|
|
||||||
|
export const uploadCheckCycle = 1000;
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
modTime: string;
|
||||||
|
isDir: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
export interface Props {
|
||||||
|
dirPath: List<string>;
|
||||||
|
items: List<MetadataResp>;
|
||||||
|
|
||||||
|
uploadFiles: List<File>;
|
||||||
|
uploadValue: string;
|
||||||
|
|
||||||
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemPath(dirPath: string, itemName: string): string {
|
||||||
|
return dirPath.endsWith("/")
|
||||||
|
? `${dirPath}${itemName}`
|
||||||
|
: `${dirPath}/${itemName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Updater {
|
||||||
|
private static props: Props;
|
||||||
|
private static usersClient: IUsersClient;
|
||||||
|
private static filesClient: IFilesClient;
|
||||||
|
|
||||||
|
static init = (props: Props) => (Updater.props = { ...props });
|
||||||
|
static setClients(usersClient: IUsersClient, filesClient: IFilesClient) {
|
||||||
|
Updater.usersClient = usersClient;
|
||||||
|
Updater.filesClient = filesClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setItems = async (dirParts: List<string>): Promise<void> => {
|
||||||
|
let dirPath = dirParts.join("/");
|
||||||
|
let listResp = await Updater.filesClient.list(dirPath);
|
||||||
|
|
||||||
|
Updater.props.dirPath = dirParts;
|
||||||
|
Updater.props.items =
|
||||||
|
listResp != null
|
||||||
|
? List<MetadataResp>(listResp.data.metadatas)
|
||||||
|
: Updater.props.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
static mkDir = async (dirPath: string): Promise<void> => {
|
||||||
|
let resp = await Updater.filesClient.mkdir(dirPath);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
alert(`failed to make dir ${dirPath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static delete = async (
|
||||||
|
dirParts: List<string>,
|
||||||
|
items: List<MetadataResp>,
|
||||||
|
selectedItems: Map<string, boolean>
|
||||||
|
): Promise<void> => {
|
||||||
|
const delRequests = items
|
||||||
|
.filter((item) => {
|
||||||
|
return selectedItems.has(item.name);
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
async (selectedItem: MetadataResp): Promise<string> => {
|
||||||
|
const itemPath = getItemPath(dirParts.join("/"), selectedItem.name);
|
||||||
|
const resp = await Updater.filesClient.delete(itemPath);
|
||||||
|
return resp.status === 200 ? "" : selectedItem.name;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const failedFiles = await Promise.all(delRequests);
|
||||||
|
failedFiles.forEach((failedFile) => {
|
||||||
|
if (failedFile !== "") {
|
||||||
|
alert(`failed to delete ${failedFile}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Updater.setItems(dirParts);
|
||||||
|
};
|
||||||
|
|
||||||
|
static moveHere = async (
|
||||||
|
srcDir: string,
|
||||||
|
dstDir: string,
|
||||||
|
selectedItems: Map<string, boolean>
|
||||||
|
): Promise<void> => {
|
||||||
|
const moveRequests = List<string>(selectedItems.keys()).map(
|
||||||
|
async (itemName: string): Promise<string> => {
|
||||||
|
const oldPath = getItemPath(srcDir, itemName);
|
||||||
|
const newPath = getItemPath(dstDir, itemName);
|
||||||
|
|
||||||
|
const resp = await Updater.filesClient.move(oldPath, newPath);
|
||||||
|
return resp.status === 200 ? "" : itemName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const failedFiles = await Promise.all(moveRequests);
|
||||||
|
failedFiles.forEach((failedItem) => {
|
||||||
|
if (failedItem !== "") {
|
||||||
|
alert(`failed to move ${failedItem}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Updater.setItems(List<string>(dstDir.split("/")));
|
||||||
|
};
|
||||||
|
|
||||||
|
static setPwd = async (oldPwd: string, newPwd: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.usersClient.setPwd(oldPwd, newPwd);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static addUploadFiles = (fileList: FileList, len: number) => {
|
||||||
|
let newUploads = List<File>([]);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
newUploads = newUploads.push(fileList.item(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
Updater.props.uploadFiles = Updater.props.uploadFiles.concat(newUploads);
|
||||||
|
};
|
||||||
|
|
||||||
|
static setUploadFiles = (uploadFiles: List<File>) => {
|
||||||
|
Updater.props.uploadFiles = uploadFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
static setBrowser = (prevState: ICoreState): ICoreState => {
|
||||||
|
prevState.panel.browser = { ...prevState.panel, ...Updater.props };
|
||||||
|
return prevState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
inputValue: string;
|
||||||
|
selectedSrc: string;
|
||||||
|
selectedItems: Map<string, boolean>;
|
||||||
|
|
||||||
|
show: boolean;
|
||||||
|
oldPwd: string;
|
||||||
|
newPwd1: string;
|
||||||
|
newPwd2: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Browser extends React.Component<Props, State, {}> {
|
||||||
|
private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
private uploadInput: Element | Text;
|
||||||
|
private assignInput: (input: Element) => void;
|
||||||
|
private onClickUpload: () => void;
|
||||||
|
private uploading: boolean;
|
||||||
|
|
||||||
|
constructor(p: Props) {
|
||||||
|
super(p);
|
||||||
|
Updater.init(p);
|
||||||
|
Updater.setClients(new UsersClient(""), new FilesClient(""));
|
||||||
|
this.update = p.update;
|
||||||
|
this.state = {
|
||||||
|
inputValue: "",
|
||||||
|
selectedSrc: "",
|
||||||
|
selectedItems: Map<string, boolean>(),
|
||||||
|
show: false,
|
||||||
|
oldPwd: "",
|
||||||
|
newPwd1: "",
|
||||||
|
newPwd2: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uploadInput = undefined;
|
||||||
|
this.assignInput = (input) => {
|
||||||
|
this.uploadInput = ReactDOM.findDOMNode(input);
|
||||||
|
};
|
||||||
|
this.onClickUpload = () => {
|
||||||
|
const uploadInput = this.uploadInput as HTMLButtonElement;
|
||||||
|
uploadInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
Updater.setItems(p.dirPath).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(this.pollUploads, uploadCheckCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollUploads = () => {
|
||||||
|
if (this.props.uploadFiles.size > 0 && !this.uploading) {
|
||||||
|
this.uploading = true;
|
||||||
|
const file = this.props.uploadFiles.get(0);
|
||||||
|
Updater.setUploadFiles(this.props.uploadFiles.slice(1));
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
|
||||||
|
const uploader = new FileUploader(
|
||||||
|
file,
|
||||||
|
`${this.props.dirPath.join("/")}/${file.name}`,
|
||||||
|
this.updateProgress
|
||||||
|
);
|
||||||
|
|
||||||
|
uploader.start().then((ok: boolean) => {
|
||||||
|
Updater.setItems(this.props.dirPath).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
alert(`upload failed: ${uploader.err()}`);
|
||||||
|
}
|
||||||
|
this.uploading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProgress = (filePath: string, progress: number) => {
|
||||||
|
// update uploading progress in the core state
|
||||||
|
};
|
||||||
|
|
||||||
|
showPane = () => {
|
||||||
|
this.setState({ show: !this.state.show });
|
||||||
|
};
|
||||||
|
changeOldPwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ oldPwd: ev.target.value });
|
||||||
|
};
|
||||||
|
changeNewPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newPwd1: ev.target.value });
|
||||||
|
};
|
||||||
|
changeNewPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newPwd2: ev.target.value });
|
||||||
|
};
|
||||||
|
onInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ inputValue: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
addUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
Updater.addUploadFiles(event.target.files, event.target.files.length);
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
};
|
||||||
|
|
||||||
|
select = (itemName: string) => {
|
||||||
|
const selectedItems = this.state.selectedItems.has(itemName)
|
||||||
|
? this.state.selectedItems.delete(itemName)
|
||||||
|
: this.state.selectedItems.set(itemName, true);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedSrc: this.props.dirPath.join("/"),
|
||||||
|
selectedItems: selectedItems,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMkDir = () => {
|
||||||
|
Updater.mkDir(this.state.inputValue)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ inputValue: "" });
|
||||||
|
return Updater.setItems(this.props.dirPath);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
delete = () => {
|
||||||
|
if (this.props.dirPath.join("/") !== this.state.selectedSrc) {
|
||||||
|
alert("please select file or folder to delete at first");
|
||||||
|
this.setState({
|
||||||
|
selectedSrc: this.props.dirPath.join("/"),
|
||||||
|
selectedItems: Map<string, boolean>(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Updater.delete(
|
||||||
|
this.props.dirPath,
|
||||||
|
this.props.items,
|
||||||
|
this.state.selectedItems
|
||||||
|
).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
this.setState({
|
||||||
|
selectedSrc: "",
|
||||||
|
selectedItems: Map<string, boolean>(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
gotoChild = (childDirName: string) => {
|
||||||
|
this.chdir(this.props.dirPath.push(childDirName));
|
||||||
|
};
|
||||||
|
|
||||||
|
chdir = (dirPath: List<string>) => {
|
||||||
|
Updater.setItems(dirPath).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
moveHere = () => {
|
||||||
|
const oldDir = this.state.selectedSrc;
|
||||||
|
const newDir = this.props.dirPath.join("/");
|
||||||
|
if (oldDir === newDir) {
|
||||||
|
alert("source directory is same as destination directory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Updater.moveHere(
|
||||||
|
this.state.selectedSrc,
|
||||||
|
this.props.dirPath.join("/"),
|
||||||
|
this.state.selectedItems
|
||||||
|
).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
this.setState({
|
||||||
|
selectedSrc: "",
|
||||||
|
selectedItems: Map<string, boolean>(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setPwd = () => {
|
||||||
|
if (this.state.newPwd1 !== this.state.newPwd2) {
|
||||||
|
alert("new passwords are not same");
|
||||||
|
} else if (this.state.newPwd1 == "") {
|
||||||
|
alert("new passwords can not be empty");
|
||||||
|
} else if (this.state.oldPwd == this.state.newPwd1) {
|
||||||
|
alert("old and new passwords are same");
|
||||||
|
} else {
|
||||||
|
Updater.setPwd(this.state.oldPwd, this.state.newPwd1).then(
|
||||||
|
(ok: boolean) => {
|
||||||
|
if (ok) {
|
||||||
|
alert("Password is updated");
|
||||||
|
} else {
|
||||||
|
alert("fail to update password");
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
oldPwd: "",
|
||||||
|
newPwd1: "",
|
||||||
|
newPwd2: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const breadcrumb = this.props.dirPath.map(
|
||||||
|
(pathPart: string, key: number) => {
|
||||||
|
return (
|
||||||
|
<span key={pathPart}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.chdir(this.props.dirPath.slice(0, key + 1))}
|
||||||
|
className="white-font margin-r-m"
|
||||||
|
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
|
||||||
|
>
|
||||||
|
{pathPart}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ops = (
|
||||||
|
<div>
|
||||||
|
<div className="grey0-font">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.delete()}
|
||||||
|
className="red0-bg white-font margin-m"
|
||||||
|
>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
<span className="margin-s">-</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.moveHere()}
|
||||||
|
className="grey1-bg white-font margin-m"
|
||||||
|
>
|
||||||
|
Paste
|
||||||
|
</button>
|
||||||
|
<span className="margin-s">-</span>
|
||||||
|
<button
|
||||||
|
onClick={this.onClickUpload}
|
||||||
|
className="green0-bg white-font margin-m"
|
||||||
|
>
|
||||||
|
Upload Files
|
||||||
|
</button>
|
||||||
|
<span className="margin-s">-</span>
|
||||||
|
<span className="margin-m">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
value={this.state.inputValue}
|
||||||
|
className="margin-r-m black0-font"
|
||||||
|
placeholder="folder name"
|
||||||
|
/>
|
||||||
|
<button onClick={this.onMkDir} className="grey1-bg white-font">
|
||||||
|
Create Folder
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={this.addUploadFile}
|
||||||
|
multiple={true}
|
||||||
|
value={this.props.uploadValue}
|
||||||
|
ref={this.assignInput}
|
||||||
|
className="black0-font hidden margin-m"
|
||||||
|
/>
|
||||||
|
<span className="margin-s">-</span>
|
||||||
|
<button
|
||||||
|
onClick={this.showPane}
|
||||||
|
className="grey1-bg white-font margin-m"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{ display: this.state.show ? "inherit" : "none" }}
|
||||||
|
className="margin-t-m"
|
||||||
|
>
|
||||||
|
<h3 className="padding-l-s grey0-font">Update Password</h3>
|
||||||
|
<input
|
||||||
|
name="old_pwd"
|
||||||
|
type="password"
|
||||||
|
onChange={this.changeOldPwd}
|
||||||
|
value={this.state.oldPwd}
|
||||||
|
className="margin-m black0-font"
|
||||||
|
placeholder="old password"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="new_pwd1"
|
||||||
|
type="password"
|
||||||
|
onChange={this.changeNewPwd1}
|
||||||
|
value={this.state.newPwd1}
|
||||||
|
className="margin-m black0-font"
|
||||||
|
placeholder="new password"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="new_pwd2"
|
||||||
|
type="password"
|
||||||
|
onChange={this.changeNewPwd2}
|
||||||
|
value={this.state.newPwd2}
|
||||||
|
className="margin-m black0-font"
|
||||||
|
placeholder="new password again"
|
||||||
|
/>
|
||||||
|
<button onClick={this.setPwd} className="grey1-bg white-font">
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemList = this.props.items.map((item: MetadataResp) => {
|
||||||
|
const isSelected = this.state.selectedItems.has(item.name);
|
||||||
|
const dirPath = this.props.dirPath.join("/");
|
||||||
|
const itemPath = dirPath.endsWith("/")
|
||||||
|
? `${dirPath}${item.name}`
|
||||||
|
: `${dirPath}/${item.name}`;
|
||||||
|
|
||||||
|
return item.isDir ? (
|
||||||
|
<tr key={item.name} className={`${isSelected ? "green0-bg" : ""}`}>
|
||||||
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
|
<span className="dot yellow0-bg"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className="item-name"
|
||||||
|
onClick={() => this.gotoChild(item.name)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>N/A</td>
|
||||||
|
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.select(item.name)}
|
||||||
|
className="grey1-bg white-font margin-t-m margin-b-m"
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<tr key={item.name} className={`${isSelected ? "green0-bg" : ""}`}>
|
||||||
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
|
<span className="dot green0-bg"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
className="item-name"
|
||||||
|
href={`/v1/fs/files?fp=${itemPath}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{Filesize(item.size, {round: 0})}</td>
|
||||||
|
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.select(item.name)}
|
||||||
|
className="grey1-bg white-font margin-t-m margin-b-m"
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="op-bar" className="op-bar">
|
||||||
|
<div className="margin-l-m margin-r-m">{ops}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="item-list" className="">
|
||||||
|
<div className="margin-b-l">{breadcrumb}</div>
|
||||||
|
<table>
|
||||||
|
<thead style={{ fontWeight: "bold" }}>
|
||||||
|
<tr>
|
||||||
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
|
<span className="dot black-bg"></span>
|
||||||
|
</td>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>File Size</td>
|
||||||
|
<td>Mod Time</td>
|
||||||
|
<td>Op</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{itemList}</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
32
src/client/web/src/components/core_state.ts
Normal file
32
src/client/web/src/components/core_state.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { List } from "immutable";
|
||||||
|
|
||||||
|
import { Props as PanelProps } from "./panel";
|
||||||
|
import { Item } from "./browser";
|
||||||
|
|
||||||
|
|
||||||
|
export interface IContext {
|
||||||
|
update: (targetStatePatch: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICoreState {
|
||||||
|
ctx: IContext;
|
||||||
|
panel: PanelProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init(): ICoreState {
|
||||||
|
return {
|
||||||
|
ctx: null,
|
||||||
|
panel: {
|
||||||
|
displaying: "browser",
|
||||||
|
authPane: {
|
||||||
|
authed: false,
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
dirPath: List<string>(["."]),
|
||||||
|
items: List<Item>([]),
|
||||||
|
uploadValue: "",
|
||||||
|
uploadFiles: List<File>([]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
68
src/client/web/src/components/panel.tsx
Normal file
68
src/client/web/src/components/panel.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ICoreState } from "./core_state";
|
||||||
|
import { Browser, Props as BrowserProps } from "./browser";
|
||||||
|
import { AuthPane, Props as AuthPaneProps } from "./auth_pane";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
displaying: string;
|
||||||
|
browser: BrowserProps;
|
||||||
|
authPane: AuthPaneProps;
|
||||||
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Updater {
|
||||||
|
private static props: Props;
|
||||||
|
|
||||||
|
static init = (props: Props) => (Updater.props = { ...props });
|
||||||
|
|
||||||
|
static setPanel = (prevState: ICoreState): ICoreState => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
panel: { ...prevState.panel, ...Updater.props },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {}
|
||||||
|
export class Panel extends React.Component<Props, State, {}> {
|
||||||
|
private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
constructor(p: Props) {
|
||||||
|
super(p);
|
||||||
|
Updater.init(p);
|
||||||
|
this.update = p.update;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="theme-white desktop">
|
||||||
|
<div id="bg" className="bg bg-img font-m">
|
||||||
|
<div
|
||||||
|
id="top-bar"
|
||||||
|
className="top-bar cyan1-font padding-t-m padding-b-m padding-l-l padding-r-l"
|
||||||
|
>
|
||||||
|
<div className="flex-2col-parent">
|
||||||
|
<span className="flex-13col h5">Quickshare</span>
|
||||||
|
<span className="flex-23col text-right">
|
||||||
|
<AuthPane
|
||||||
|
authed={this.props.authPane.authed}
|
||||||
|
update={this.update}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="container-center">
|
||||||
|
<Browser
|
||||||
|
dirPath={this.props.browser.dirPath}
|
||||||
|
items={this.props.browser.items}
|
||||||
|
update={this.update}
|
||||||
|
uploadFiles={this.props.browser.uploadFiles}
|
||||||
|
uploadValue={this.props.browser.uploadValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
src/client/web/src/components/state_mgr.tsx
Normal file
37
src/client/web/src/components/state_mgr.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ICoreState, init } from "./core_state";
|
||||||
|
import { Panel } from "./panel";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
export interface State extends ICoreState {}
|
||||||
|
|
||||||
|
export class UpdaterBase {
|
||||||
|
private static props: any;
|
||||||
|
static init = (props: any) => (UpdaterBase.props = {...props});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StateMgr extends React.Component<Props, State, {}> {
|
||||||
|
constructor(p: Props) {
|
||||||
|
super(p);
|
||||||
|
this.state = init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: any can be eliminated by adding union type of children states
|
||||||
|
update = (updater: (prevState:ICoreState) => ICoreState): void => {
|
||||||
|
console.log("before", this.state)
|
||||||
|
this.setState(updater(this.state));
|
||||||
|
console.log("after", this.state)
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
authPane = {this.state.panel.authPane}
|
||||||
|
displaying={this.state.panel.displaying}
|
||||||
|
update={this.update}
|
||||||
|
browser={this.state.panel.browser}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
99
src/client/web/src/theme/animation.css
Normal file
99
src/client/web/src/theme/animation.css
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
.anm-rotate {
|
||||||
|
animation: rotate 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
20% {
|
||||||
|
transform: rotate(72deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: rotate(144deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: rotate(216deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: rotate(288deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
/* animation-direction: reverse; */
|
||||||
|
animation: fade-in 0.2s 1 linear;
|
||||||
|
opacity: 0.5;
|
||||||
|
/* padding: 0; */
|
||||||
|
transform: translate(0, 0.4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translate(0, 0.4rem);
|
||||||
|
/* padding: 0.4rem 0; */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
/* transform: translate(0, 0.4rem); */
|
||||||
|
/* padding: 0; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-resize {
|
||||||
|
animation: resize 2s 1 linear;
|
||||||
|
/* transform: translate(100%, 0%); */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes resize {
|
||||||
|
0% {
|
||||||
|
/* transform: translate(0, 0); */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1, 0);
|
||||||
|
}
|
||||||
|
5% {
|
||||||
|
/* transform: translate(0, 8rem); */
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1, 1);
|
||||||
|
}
|
||||||
|
95% {
|
||||||
|
/* transform: translate(0, 8rem); */
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1, 1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
/* transform: translate(0, 0rem); */
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-cool-down {
|
||||||
|
animation: cooldown 0.3s 1 linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cooldown {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-trans {
|
||||||
|
transition: opacity 0.15s linear;
|
||||||
|
}
|
375
src/client/web/src/theme/color.css
Normal file
375
src/client/web/src/theme/color.css
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
.blue0-font {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue1-font {
|
||||||
|
color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyan0-font {
|
||||||
|
color: #1abc9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyan1-font {
|
||||||
|
color: #16a085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple0-font {
|
||||||
|
color: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple1-font {
|
||||||
|
color: #8e44ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red0-font {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red1-font {
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow0-font {
|
||||||
|
color: #f1c40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow1-font {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow2-font {
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow3-font {
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green0-font {
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green1-font {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-font {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white0-font {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white1-font {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey0-font {
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey1-font {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-font {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black0-font {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black1-font {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue0-bg {
|
||||||
|
background-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue1-bg {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyan0-bg {
|
||||||
|
background-color: #1abc9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyan1-bg {
|
||||||
|
background-color: #16a085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple0-bg {
|
||||||
|
background-color: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple1-bg {
|
||||||
|
background-color: #8e44ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red0-bg {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red1-bg {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow0-bg {
|
||||||
|
background-color: #f1c40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow1-bg {
|
||||||
|
background-color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow2-bg {
|
||||||
|
background-color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow3-bg {
|
||||||
|
background-color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green0-bg {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green1-bg {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-bg {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white0-bg {
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white1-bg {
|
||||||
|
background-color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey0-bg {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey1-bg {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-bg {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black0-bg {
|
||||||
|
background-color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black1-bg {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-xs {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-s {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-m {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-xl {
|
||||||
|
padding: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l-xs {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l-s {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l-m {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l-l {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-l-xl {
|
||||||
|
padding-left: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-r-xs {
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-r-s {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-r-m {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-r-l {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-r-xl {
|
||||||
|
padding-right: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-t-xs {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-t-s {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-t-m {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-t-l {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-t-xl {
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-b-xs {
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-b-s {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-b-m {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-b-l {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-b-xl {
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-xs {
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-s {
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-m {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l {
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-xl {
|
||||||
|
margin: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l-xs {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l-s {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l-m {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l-l {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-l-xl {
|
||||||
|
margin-left: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-r-xs {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-r-s {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-r-m {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-r-l {
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-r-xl {
|
||||||
|
margin-right: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-t-xs {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-t-s {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-t-m {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-t-l {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-t-xl {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-b-xs {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-b-s {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-b-m {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-b-l {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-b-xl {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
24
src/client/web/src/theme/desktop.css
Normal file
24
src/client/web/src/theme/desktop.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
.desktop .font-xs {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop .font-s {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop .font-m {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop .font-l {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 2.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop .font-xl {
|
||||||
|
font-size: 2.0rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
118
src/client/web/src/theme/reset.css
Normal file
118
src/client/web/src/theme/reset.css
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
/* background: url("bg.jpg") no-repeat left center fixed;
|
||||||
|
background-size: auto 100%; */
|
||||||
|
font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 200%;
|
||||||
|
}
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:link,
|
||||||
|
a:visited,
|
||||||
|
a:hover,
|
||||||
|
a:active,
|
||||||
|
button,
|
||||||
|
img {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #16a085;
|
||||||
|
opacity: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 1s 1 linear;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
a:active {
|
||||||
|
}
|
||||||
|
a::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
/* animation: fade 0.1s 1 linear; */
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
/* animation: fade 0.1s 1 linear; */
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
width: 10rem;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
486
src/client/web/src/theme/style.css
Normal file
486
src/client/web/src/theme/style.css
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
#bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-bar {
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-center {
|
||||||
|
margin: 5rem auto auto auto;
|
||||||
|
width: 96%;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#op-bar {
|
||||||
|
width: 96%;
|
||||||
|
max-width: 960px;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 6rem;
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
line-height: 3rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-list {
|
||||||
|
margin-top: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-list table {
|
||||||
|
width: 100%;
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
margin-bottom: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-list tr {
|
||||||
|
border-top: solid 1px transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-list .item-name {
|
||||||
|
display: block;
|
||||||
|
max-width: 8rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-list tr:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 4rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 6rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
#panel-head {
|
||||||
|
text-align: left;
|
||||||
|
/* box-shadow: 0 0.2rem 3rem rgba(0, 0, 0, 0.1); */
|
||||||
|
|
||||||
|
padding: 1rem 2rem 1rem 2rem;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
font-size: 2.0rem;
|
||||||
|
line-height: 3.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel-head-menu {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
/* height: 3rem; */
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 4rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
/* display: flex;
|
||||||
|
justify-content: space-between; */
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
/* background-color: rgba(255, 255, 255, 0.6); */
|
||||||
|
}
|
||||||
|
.text-header {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #16a085;
|
||||||
|
margin: auto;
|
||||||
|
line-height: 4rem;
|
||||||
|
}
|
||||||
|
#panel-body {
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 6rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
left: 0;
|
||||||
|
/* box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.1); */
|
||||||
|
}
|
||||||
|
#nav-bar {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 0 1rem 0;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 5rem;
|
||||||
|
box-shadow: 0 -0.2rem 3rem rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.nav-bar-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.win-left-bar {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
/* right: auto; */
|
||||||
|
/* bottom: 0.5rem; */
|
||||||
|
left: -0.5rem;
|
||||||
|
/* box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.1); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.win-right-bar {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
/* right: auto; */
|
||||||
|
/* bottom: 0.5rem; */
|
||||||
|
right: -0.5rem;
|
||||||
|
/* box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.1); */
|
||||||
|
}
|
||||||
|
.right-bar {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
|
||||||
|
width: 20rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 2rem;
|
||||||
|
top: 4rem;
|
||||||
|
}
|
||||||
|
.link-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.ad-wrap {
|
||||||
|
padding: 0 0 1rem 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ad-wrap-title {
|
||||||
|
line-height: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* layoout */
|
||||||
|
.flex-2col-parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.flex-2col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
.flex-23col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 66%;
|
||||||
|
}
|
||||||
|
.flex-13col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 31%;
|
||||||
|
}
|
||||||
|
.flex-col-parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.clearfix {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
text-align: left;
|
||||||
|
/* margin-bottom: 2rem; */
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.section-h {
|
||||||
|
text-align: left;
|
||||||
|
/* margin-bottom: 2rem; */
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-grey {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.margin-m {
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
.margin-right-s {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.margin-right-m {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.margin-l {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
.margin-h-l {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.margin-h-m {
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}
|
||||||
|
.margin-right-l {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.margin-left-s {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
.margin-left-m {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.margin-left-l {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* font */
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.weight-normal {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.h1,
|
||||||
|
.h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.h3,
|
||||||
|
.h4 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.h5,
|
||||||
|
.h6 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
|
||||||
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* component */
|
||||||
|
.icon-s {
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
margin: -0.2rem 0.5rem auto 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
|
.pane {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 8rem 0 10rem 0;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.menu-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4rem;
|
||||||
|
}
|
||||||
|
.wide-btn {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 2rem 2rem 6rem 2rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.notification-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 10rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
max-height: 10rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
.notification {
|
||||||
|
/* color: white; */
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
/* font-weight: bold; */
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin: 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
.notification-ok {
|
||||||
|
color: white;
|
||||||
|
fill: #2ecc71;
|
||||||
|
}
|
||||||
|
.notification-error {
|
||||||
|
color: white;
|
||||||
|
fill: #e74c3c;
|
||||||
|
}
|
||||||
|
.notification-warn {
|
||||||
|
color: white;
|
||||||
|
fill: #f1c40f;
|
||||||
|
}
|
||||||
|
.font-size-m {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.news {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.news .title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.news .desc {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
}
|
||||||
|
.bottom-line {
|
||||||
|
padding: 3rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #16a085;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: white;
|
||||||
|
border: transparent;
|
||||||
|
color: black;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 0.8rem;
|
||||||
|
width: 0.8rem;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.white-square {
|
||||||
|
border: solid 2px #fff;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid .desc {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-dot {
|
||||||
|
position: relative;
|
||||||
|
right: -1rem;
|
||||||
|
top: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.hr {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem 0;
|
||||||
|
} */
|
69
src/client/web/src/theme/white.css
Normal file
69
src/client/web/src/theme/white.css
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
.theme-white .bg {
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .text-color {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .bg-img {
|
||||||
|
background: url("/static/img/textured_paper.png") repeat fixed center;
|
||||||
|
/* background: url("/static/img/huangpu.jpg") repeat fixed center; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .top-bar {
|
||||||
|
background: rgba( 255, 255, 255, 0.9 );
|
||||||
|
box-shadow: 0 5px 30px 0 rgba( 31, 38, 135, 0.1 );
|
||||||
|
backdrop-filter: blur( 9.5px );
|
||||||
|
-webkit-backdrop-filter: blur( 9.5px );
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white div.hr {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .op-bar {
|
||||||
|
background: rgba( 255, 255, 255, 0.9 );
|
||||||
|
box-shadow: 0 5px 30px 0 rgba( 31, 38, 135, 0.1 );
|
||||||
|
backdrop-filter: blur( 9.5px );
|
||||||
|
-webkit-backdrop-filter: blur( 9.5px );
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .panel {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .panel-head {
|
||||||
|
/* background-color: white; */
|
||||||
|
border-bottom: solid 1px #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .panel-head-menu {
|
||||||
|
background-color: white;
|
||||||
|
color: #34495e;
|
||||||
|
border-bottom: solid 1px #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .panel-body {
|
||||||
|
/* background-color: #fafafa; */
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .nav-bar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #7f8c8d;
|
||||||
|
fill: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .news .title {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .border {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-white .wide-btn {
|
||||||
|
color: white;
|
||||||
|
background-color: #16a085;
|
||||||
|
}
|
13
src/client/web/tsconfig.json
Normal file
13
src/client/web/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"jsx": "react",
|
||||||
|
"lib": ["es2015", "dom"]
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*", "webpack.config.js", "webpack..js"],
|
||||||
|
"exclude": ["**/*.test.ts", "**/*.test.tsx"]
|
||||||
|
}
|
21
src/client/web/webpack.app.dev.js
Normal file
21
src/client/web/webpack.app.dev.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
const dev = require("./webpack.dev.js");
|
||||||
|
|
||||||
|
module.exports = merge(dev, {
|
||||||
|
entry: "./src/app.tsx",
|
||||||
|
context: `${__dirname}`,
|
||||||
|
output: {
|
||||||
|
path: `${__dirname}/../../../public/static`,
|
||||||
|
chunkFilename: "[name].bundle.js",
|
||||||
|
filename: "[name].bundle.js"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: `${__dirname}/build/template/index.template.dev.html`,
|
||||||
|
hash: true,
|
||||||
|
filename: `../index.html`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
21
src/client/web/webpack.app.prod.js
Normal file
21
src/client/web/webpack.app.prod.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
const prod = require("./webpack.prod.js");
|
||||||
|
|
||||||
|
module.exports = merge(prod, {
|
||||||
|
entry: "./src/app.tsx",
|
||||||
|
context: `${__dirname}`,
|
||||||
|
output: {
|
||||||
|
path: `${__dirname}/../../../public/static`,
|
||||||
|
chunkFilename: "[name].bundle.js",
|
||||||
|
filename: "[name].bundle.js"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: `${__dirname}/build/template/index.template.html`,
|
||||||
|
hash: true,
|
||||||
|
filename: `../index.html`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
63
src/client/web/webpack.common.js
Normal file
63
src/client/web/webpack.common.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// const webpack = require("webpack");
|
||||||
|
// const CleanWebpackPlugin = require("clean-webpack-plugin");
|
||||||
|
// const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
|
||||||
|
const path = require("path");
|
||||||
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||||
|
.BundleAnalyzerPlugin;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts|tsx$/,
|
||||||
|
loader: "ts-loader",
|
||||||
|
include: [path.resolve(__dirname, "src")],
|
||||||
|
exclude: /\.test\.(ts|tsx)$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
url: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".tsx", ".js", ".json"]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// new BundleAnalyzerPlugin()
|
||||||
|
],
|
||||||
|
externals: {
|
||||||
|
react: "React",
|
||||||
|
"react-dom": "ReactDOM",
|
||||||
|
immutable: "Immutable"
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [new TerserPlugin()],
|
||||||
|
splitChunks: {
|
||||||
|
chunks: "all",
|
||||||
|
automaticNameDelimiter: ".",
|
||||||
|
cacheGroups: {
|
||||||
|
default: {
|
||||||
|
name: "main",
|
||||||
|
filename: "[name].bundle.js"
|
||||||
|
},
|
||||||
|
commons: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: "vendors",
|
||||||
|
chunks: "all",
|
||||||
|
minChunks: 2,
|
||||||
|
reuseExistingChunk: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
16
src/client/web/webpack.dev.js
Normal file
16
src/client/web/webpack.dev.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
const common = require("./webpack.common.js");
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: "development",
|
||||||
|
devtool: "inline-source-map",
|
||||||
|
// entry: {
|
||||||
|
// api_test: "./libs/test/api_test"
|
||||||
|
// },
|
||||||
|
watchOptions: {
|
||||||
|
aggregateTimeout: 1000,
|
||||||
|
poll: 1000,
|
||||||
|
ignored: /node_modules/
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
});
|
8
src/client/web/webpack.prod.js
Normal file
8
src/client/web/webpack.prod.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// const webpack = require("webpack");
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
|
||||||
|
const common = require("./webpack.common.js");
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: "production"
|
||||||
|
});
|
5874
src/client/web/yarn.lock
Normal file
5874
src/client/web/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -135,7 +135,7 @@ func (fs *LocalFS) Remove(entryPath string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.Remove(fullpath)
|
return os.RemoveAll(fullpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *LocalFS) Rename(oldpath, newpath string) error {
|
func (fs *LocalFS) Rename(oldpath, newpath string) error {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package fileshdr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -32,6 +33,8 @@ var (
|
||||||
rangeHeader = "Range"
|
rangeHeader = "Range"
|
||||||
acceptRangeHeader = "Accept-Range"
|
acceptRangeHeader = "Accept-Range"
|
||||||
ifRangeHeader = "If-Range"
|
ifRangeHeader = "If-Range"
|
||||||
|
keepAliveHeader = "Keep-Alive"
|
||||||
|
connectionHeader = "Connection"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileHandlers struct {
|
type FileHandlers struct {
|
||||||
|
@ -73,17 +76,22 @@ func (h *FileHandlers) NewAutoLocker(c *gin.Context, key string) *AutoLocker {
|
||||||
func (lk *AutoLocker) Exec(handler func()) {
|
func (lk *AutoLocker) Exec(handler func()) {
|
||||||
var err error
|
var err error
|
||||||
kv := lk.h.deps.KV()
|
kv := lk.h.deps.KV()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
fmt.Println(p)
|
||||||
|
}
|
||||||
|
if err = kv.Unlock(lk.key); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err = kv.TryLock(lk.key); err != nil {
|
if err = kv.TryLock(lk.key); err != nil {
|
||||||
lk.c.JSON(q.Resp(500))
|
lk.c.JSON(q.ErrResp(lk.c, 500, errors.New("fail to lock the file")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handler()
|
handler()
|
||||||
|
|
||||||
if err = kv.Unlock(lk.key); err != nil {
|
|
||||||
// TODO: use logger
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateReq struct {
|
type CreateReq struct {
|
||||||
|
@ -118,9 +126,9 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
c.JSON(q.Resp(200))
|
c.JSON(q.Resp(200))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *FileHandlers) Delete(c *gin.Context) {
|
func (h *FileHandlers) Delete(c *gin.Context) {
|
||||||
|
@ -255,12 +263,21 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(req.Content), req.Offset)
|
content, err := base64.StdEncoding.DecodeString(req.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.uploadMgr.IncreUploaded(tmpFilePath, int64(wrote))
|
|
||||||
|
fmt.Println("length", len([]byte(content)))
|
||||||
|
|
||||||
|
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(content), req.Offset)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.uploadMgr.SetUploaded(tmpFilePath, req.Offset+int64(wrote))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
|
@ -308,7 +325,11 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(tmpFilePath)
|
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(tmpFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(q.ErrResp(c, 404, err))
|
||||||
|
} else {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +358,7 @@ func (h *FileHandlers) Download(c *gin.Context) {
|
||||||
info, err := h.deps.FS().Stat(filePath)
|
info, err := h.deps.FS().Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
c.JSON(q.ErrResp(c, 400, os.ErrNotExist))
|
c.JSON(q.ErrResp(c, 404, os.ErrNotExist))
|
||||||
} else {
|
} else {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,17 +37,13 @@ func (um *UploadMgr) AddInfo(fileName, tmpName string, fileSize int64, isDir boo
|
||||||
return um.kv.SetString(infoKey(tmpName, filePathKey), fileName)
|
return um.kv.SetString(infoKey(tmpName, filePathKey), fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *UploadMgr) IncreUploaded(fileName string, newUploaded int64) error {
|
func (um *UploadMgr) SetUploaded(fileName string, newUploaded int64) error {
|
||||||
fileSize, ok := um.kv.GetInt64(infoKey(fileName, fileSizeKey))
|
fileSize, ok := um.kv.GetInt64(infoKey(fileName, fileSizeKey))
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("file size %s not found", fileName)
|
return fmt.Errorf("file size %s not found", fileName)
|
||||||
}
|
}
|
||||||
preUploaded, ok := um.kv.GetInt64(infoKey(fileName, uploadedKey))
|
if newUploaded <= fileSize {
|
||||||
if !ok {
|
um.kv.SetInt64(infoKey(fileName, uploadedKey), newUploaded)
|
||||||
return fmt.Errorf("file uploaded %s not found", fileName)
|
|
||||||
}
|
|
||||||
if newUploaded+preUploaded <= fileSize {
|
|
||||||
um.kv.SetInt64(infoKey(fileName, uploadedKey), newUploaded+preUploaded)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.New("uploaded is greater than file size")
|
return errors.New("uploaded is greater than file size")
|
||||||
|
|
|
@ -89,6 +89,15 @@ type LoginReq struct {
|
||||||
Pwd string `json:"pwd"`
|
Pwd string `json:"pwd"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SimpleUserHandlers) checkPwd(user, pwd string) error {
|
||||||
|
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, user)
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(pwd))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
||||||
req := &LoginReq{}
|
req := &LoginReq{}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
@ -96,15 +105,8 @@ func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User)
|
if err := h.checkPwd(req.User, req.Pwd); err != nil {
|
||||||
if !ok {
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.Pwd))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(q.ErrResp(c, 401, err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,32 +126,30 @@ func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := h.cfg.GrabString("Server.Host")
|
|
||||||
secure := h.cfg.GrabBool("Users.CookieSecure")
|
secure := h.cfg.GrabBool("Users.CookieSecure")
|
||||||
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
|
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
|
||||||
c.SetCookie(TokenCookie, token, ttl, "/", hostname, secure, httpOnly)
|
c.SetCookie(TokenCookie, token, ttl, "/", "", secure, httpOnly)
|
||||||
|
|
||||||
c.JSON(q.Resp(200))
|
c.JSON(q.Resp(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogoutReq struct {
|
type LogoutReq struct {
|
||||||
User string `json:"user"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SimpleUserHandlers) Logout(c *gin.Context) {
|
func (h *SimpleUserHandlers) Logout(c *gin.Context) {
|
||||||
req := &LogoutReq{}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// token alreay verified in the authn middleware
|
// token alreay verified in the authn middleware
|
||||||
c.SetCookie(TokenCookie, "", 0, "/", "nohost", false, true)
|
secure := h.cfg.GrabBool("Users.CookieSecure")
|
||||||
|
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
|
||||||
|
c.SetCookie(TokenCookie, "", 0, "/", "", secure, httpOnly)
|
||||||
|
c.JSON(q.Resp(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SimpleUserHandlers) IsAuthed(c *gin.Context) {
|
||||||
|
// token alreay verified in the authn middleware
|
||||||
c.JSON(q.Resp(200))
|
c.JSON(q.Resp(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetPwdReq struct {
|
type SetPwdReq struct {
|
||||||
User string `json:"user"`
|
|
||||||
OldPwd string `json:"oldPwd"`
|
OldPwd string `json:"oldPwd"`
|
||||||
NewPwd string `json:"newPwd"`
|
NewPwd string `json:"newPwd"`
|
||||||
}
|
}
|
||||||
|
@ -159,15 +159,24 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(q.ErrResp(c, 400, err))
|
c.JSON(q.ErrResp(c, 400, err))
|
||||||
return
|
return
|
||||||
|
} else if req.OldPwd == req.NewPwd {
|
||||||
|
c.JSON(q.ErrResp(c, 400, errors.New("password is not updated")))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User)
|
claims, err := h.getUserInfo(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 401, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, claims[UserParam])
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.OldPwd))
|
err = bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.OldPwd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 401, ErrInvalidUser))
|
c.JSON(q.ErrResp(c, 401, ErrInvalidUser))
|
||||||
return
|
return
|
||||||
|
@ -178,7 +187,7 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
||||||
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
|
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.deps.KV().SetStringIn(UsersNs, req.User, string(newHash))
|
err = h.deps.KV().SetStringIn(UsersNs, claims[UserParam], string(newHash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
||||||
return
|
return
|
||||||
|
@ -186,3 +195,25 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(q.Resp(200))
|
c.JSON(q.Resp(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SimpleUserHandlers) getUserInfo(c *gin.Context) (map[string]string, error) {
|
||||||
|
tokenStr, err := c.Cookie(TokenCookie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
claims, err := h.deps.Token().FromToken(
|
||||||
|
tokenStr,
|
||||||
|
map[string]string{
|
||||||
|
UserParam: "",
|
||||||
|
RoleParam: "",
|
||||||
|
ExpireParam: "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if claims[UserParam] == "" {
|
||||||
|
return nil, ErrInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,13 @@ var exposedAPIs = map[string]bool{
|
||||||
"Health-fm": true,
|
"Health-fm": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var publicRootPath = "/"
|
||||||
|
var publicStaticPath = "/static"
|
||||||
|
|
||||||
|
func IsPublicPath(accessPath string) bool {
|
||||||
|
return accessPath == publicRootPath || strings.HasPrefix(accessPath, publicStaticPath)
|
||||||
|
}
|
||||||
|
|
||||||
func GetHandlerName(fullname string) (string, error) {
|
func GetHandlerName(fullname string) (string, error) {
|
||||||
parts := strings.Split(fullname, ".")
|
parts := strings.Split(fullname, ".")
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
|
@ -30,13 +37,13 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc {
|
||||||
c.JSON(q.ErrResp(c, 401, err))
|
c.JSON(q.ErrResp(c, 401, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
accessPath := c.Request.URL.String()
|
||||||
|
|
||||||
// TODO: may also check the path
|
|
||||||
enableAuth := h.cfg.GrabBool("Users.EnableAuth")
|
enableAuth := h.cfg.GrabBool("Users.EnableAuth")
|
||||||
if enableAuth && !exposedAPIs[handlerName] {
|
if enableAuth && !exposedAPIs[handlerName] && !IsPublicPath(accessPath) {
|
||||||
token, err := c.Cookie(TokenCookie)
|
token, err := c.Cookie(TokenCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 401, err))
|
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,20 +55,20 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc {
|
||||||
|
|
||||||
_, err = h.deps.Token().FromToken(token, claims)
|
_, err = h.deps.Token().FromToken(token, claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 401, err))
|
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64)
|
expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64)
|
||||||
if err != nil || expire <= now {
|
if err != nil || expire <= now {
|
||||||
c.JSON(q.ErrResp(c, 401, err))
|
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitor is only allowed to download
|
// visitor is only allowed to download
|
||||||
if claims[RoleParam] != AdminRole && handlerName != "Download-fm" {
|
if claims[RoleParam] != AdminRole && handlerName != "Download-fm" {
|
||||||
c.JSON(q.Resp(401))
|
c.AbortWithStatusJSON(q.ErrResp(c, 401, errors.New("not allowed")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ func DefaultConfig() (string, error) {
|
||||||
OpenTTL: 60, // 1 min
|
OpenTTL: 60, // 1 min
|
||||||
},
|
},
|
||||||
Users: &UsersCfg{
|
Users: &UsersCfg{
|
||||||
EnableAuth: false,
|
EnableAuth: true,
|
||||||
DefaultAdmin: "",
|
DefaultAdmin: "",
|
||||||
DefaultAdminPwd: "",
|
DefaultAdminPwd: "",
|
||||||
CookieTTL: 3600 * 24 * 7, // 1 week
|
CookieTTL: 3600 * 24 * 7, // 1 week
|
||||||
|
@ -61,10 +61,10 @@ func DefaultConfig() (string, error) {
|
||||||
},
|
},
|
||||||
Server: &ServerCfg{
|
Server: &ServerCfg{
|
||||||
Debug: false,
|
Debug: false,
|
||||||
Host: "127.0.0.1",
|
Host: "0.0.0.0",
|
||||||
Port: 8888,
|
Port: 8888,
|
||||||
ReadTimeout: 2000,
|
ReadTimeout: 2000,
|
||||||
WriteTimeout: 2000,
|
WriteTimeout: 1000 * 3600 * 24, // 1 day
|
||||||
MaxHeaderBytes: 512,
|
MaxHeaderBytes: 512,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
|
||||||
// middleware
|
// middleware
|
||||||
router.Use(userHdrs.Auth())
|
router.Use(userHdrs.Auth())
|
||||||
// tmp static server
|
// tmp static server
|
||||||
router.Use(static.Serve("/", static.LocalFile("../static", false)))
|
router.Use(static.Serve("/", static.LocalFile("../public", false)))
|
||||||
|
|
||||||
// handler
|
// handler
|
||||||
v1 := router.Group("/v1")
|
v1 := router.Group("/v1")
|
||||||
|
@ -132,6 +132,7 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
|
||||||
usersAPI := v1.Group("/users")
|
usersAPI := v1.Group("/users")
|
||||||
usersAPI.POST("/login", userHdrs.Login)
|
usersAPI.POST("/login", userHdrs.Login)
|
||||||
usersAPI.POST("/logout", userHdrs.Logout)
|
usersAPI.POST("/logout", userHdrs.Logout)
|
||||||
|
usersAPI.GET("/isauthed", userHdrs.IsAuthed)
|
||||||
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
||||||
|
|
||||||
filesAPI := v1.Group("/fs")
|
filesAPI := v1.Group("/fs")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -60,7 +61,8 @@ func TestFileHandlers(t *testing.T) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
res, _, errs = cl.UploadChunk(filePath, content, 0)
|
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||||||
|
res, _, errs = cl.UploadChunk(filePath, base64Content, 0)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Error(errs)
|
t.Error(errs)
|
||||||
return false
|
return false
|
||||||
|
@ -172,7 +174,9 @@ func TestFileHandlers(t *testing.T) {
|
||||||
right = len(contentBytes)
|
right = len(contentBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, _, errs = cl.UploadChunk(filePath, string(contentBytes[i:right]), int64(i))
|
chunk := contentBytes[i:right]
|
||||||
|
chunkBase64 := base64.StdEncoding.EncodeToString(chunk)
|
||||||
|
res, _, errs = cl.UploadChunk(filePath, chunkBase64, int64(i))
|
||||||
i = right
|
i = right
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -193,6 +197,11 @@ func TestFileHandlers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = fs.Sync()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// check uploaded file
|
// check uploaded file
|
||||||
fsFilePath := filepath.Join(fileshdr.FsDir, filePath)
|
fsFilePath := filepath.Join(fileshdr.FsDir, filePath)
|
||||||
info, err = fs.Stat(fsFilePath)
|
info, err = fs.Stat(fsFilePath)
|
||||||
|
@ -245,6 +254,11 @@ func TestFileHandlers(t *testing.T) {
|
||||||
assertUploadOK(t, filePath, content)
|
assertUploadOK(t, filePath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = fs.Sync()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
_, lResp, errs := cl.List(dirPath)
|
_, lResp, errs := cl.List(dirPath)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -292,6 +306,11 @@ func TestFileHandlers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = fs.Sync()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
_, lResp, errs := cl.List(dstDir)
|
_, lResp, errs := cl.List(dstDir)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
|
|
@ -54,14 +54,14 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
|
|
||||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||||
|
|
||||||
resp, _, errs = suCl.SetPwd(adminName, adminPwd, adminNewPwd, token)
|
resp, _, errs = suCl.SetPwd(adminPwd, adminNewPwd, token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
} else if resp.StatusCode != 200 {
|
} else if resp.StatusCode != 200 {
|
||||||
t.Fatal(resp.StatusCode)
|
t.Fatal(resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _, errs = suCl.Logout(adminName, token)
|
resp, _, errs = suCl.Logout(token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
} else if resp.StatusCode != 200 {
|
} else if resp.StatusCode != 200 {
|
||||||
|
|
94
src/static/index.html
Normal file
94
src/static/index.html
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Quickshare</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no,uc-fitscreen=yes"
|
||||||
|
/>
|
||||||
|
<meta class="chrome-color" name="theme-color" content="black" />
|
||||||
|
<script src="/static/js/react.development.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/react-dom.development.js?v=16.8.6"></script>
|
||||||
|
<script src="/static/js/immutable.min.js?v=4.0.0-rc.12"></script>
|
||||||
|
<!-- <link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="57x57"
|
||||||
|
href="/static/fav/apple-icon-57x57.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="60x60"
|
||||||
|
href="/static/fav/apple-icon-60x60.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="72x72"
|
||||||
|
href="/static/fav/apple-icon-72x72.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="76x76"
|
||||||
|
href="/static/fav/apple-icon-76x76.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="114x114"
|
||||||
|
href="/static/fav/apple-icon-114x114.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="120x120"
|
||||||
|
href="/static/fav/apple-icon-120x120.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="144x144"
|
||||||
|
href="/static/fav/apple-icon-144x144.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="152x152"
|
||||||
|
href="/static/fav/apple-icon-152x152.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/static/fav/apple-icon-180x180.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/static/fav/android-icon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/static/fav/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="96x96"
|
||||||
|
href="/static/fav/favicon-96x96.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/static/fav/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/static/fav/manifest.json" />
|
||||||
|
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||||
|
<meta
|
||||||
|
name="msapplication-TileImage"
|
||||||
|
content="/static/fav/ms-icon-144x144.png"
|
||||||
|
/> -->
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content"><div id="mount"></div></div>
|
||||||
|
<script src="main.bundle.js?4fa055bbbe4e223c0472"></script></body>
|
||||||
|
</html>
|
|
@ -2869,6 +2869,11 @@ filesize@^3.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
|
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
|
||||||
integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
|
integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
|
||||||
|
|
||||||
|
filesize@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00"
|
||||||
|
integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==
|
||||||
|
|
||||||
fill-range@^4.0.0:
|
fill-range@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue