Merge branch 'develop'
This commit is contained in:
commit
3a384a51bd
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,4 +4,7 @@ public
|
|||||||
dist
|
dist
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
storage/tmp
|
storage/tmp
|
||||||
storage/uploads
|
storage/uploads
|
||||||
|
|
||||||
|
config/local.*
|
||||||
|
src/package.json
|
||||||
|
@ -6,9 +6,10 @@ $secondaryForeground: $primaryForeground;
|
|||||||
$backgroundColor: darken($primary, 4%);
|
$backgroundColor: darken($primary, 4%);
|
||||||
$defaultTextColor: #ffffff;
|
$defaultTextColor: #ffffff;
|
||||||
|
|
||||||
$headerBackground: darken($primary, 7.5%);
|
$headerBackground: transparent;
|
||||||
$footerBackground: lighten($headerBackground, 1%);
|
$headerContainer: true;
|
||||||
$panelBackground: lighten($headerBackground, 1%);
|
$footerBackground: transparent;
|
||||||
|
$panelBackground: darken($backgroundColor, 3.2%);
|
||||||
$inputBackground: darken($panelBackground, 4%);
|
$inputBackground: darken($panelBackground, 4%);
|
||||||
|
|
||||||
$info: #4499ff;
|
$info: #4499ff;
|
||||||
@ -28,4 +29,5 @@ $errorText: darken($error, 30%);
|
|||||||
$errorColor: desaturate($errorText, 50%);
|
$errorColor: desaturate($errorText, 50%);
|
||||||
|
|
||||||
// Responsivity
|
// Responsivity
|
||||||
$mobileThreshold: 632px;
|
$mobileThreshold: 850px;
|
||||||
|
$desktopThreshold: 940px;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@import "vars";
|
@import "vars";
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
|
@import "responsivity_tools";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -69,21 +70,31 @@ body {
|
|||||||
body > header {
|
body > header {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
$headerHeight: 64px;
|
$headerHeight: 64px;
|
||||||
height: $headerHeight;
|
height: $headerHeight;
|
||||||
line-height: $headerHeight;
|
line-height: $headerHeight;
|
||||||
|
|
||||||
background-color: $headerBackground;
|
background: $headerBackground;
|
||||||
|
|
||||||
|
@if $headerContainer {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobileThreshold) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
padding: 0 24px 0 16px;
|
padding: 0 16px 0 8px;
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
color: $defaultTextColor;
|
color: $defaultTextColor;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -91,34 +102,48 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: $headerHeight;
|
width: initial;
|
||||||
height: $headerHeight;
|
height: calc(#{$headerHeight} - 16px);
|
||||||
margin-right: 16px;
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
ul {
|
> ul {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform ease-out 150ms;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: $headerHeight 8px 8px;
|
||||||
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
|
background: $panelBackground;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
a, button {
|
a, button {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 64px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 24px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
&:hover, &:active {
|
&:hover, &:active {
|
||||||
&:not(button) {
|
&:not(button) {
|
||||||
@ -127,13 +152,40 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
--icon-size: 24px;
|
--icon-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
position: static;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
color: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: inherit;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 8px;
|
margin: 0;
|
||||||
padding: 24px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
@ -158,114 +210,117 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: absolute;
|
position: initial;
|
||||||
z-index: -1;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
background: $headerBackground;
|
|
||||||
border-radius: 0 0 3px 3px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .dropdown {
|
|
||||||
display: block;
|
display: block;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> li:not(:first-child) {
|
||||||
|
border-top: 1px solid transparentize($defaultTextColor, 0.8);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: translateX(0%);
|
||||||
|
box-shadow: 0 0 5px darken($panelBackground, 20%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-button {
|
#menu-button {
|
||||||
display: none;
|
position: fixed;
|
||||||
}
|
top: 0;
|
||||||
}
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
line-height: $headerHeight;
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
cursor: pointer;
|
||||||
flex-direction: row-reverse;
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
.logo {
|
.feather {
|
||||||
padding: 0 16px 0 8px;
|
--icon-size: 28px;
|
||||||
font-size: 24px;
|
margin: 0 8px;
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
hr {
|
||||||
#menu-button {
|
border: 0;
|
||||||
display: block;
|
border-bottom: 1px solid $defaultTextColor;
|
||||||
margin: 0;
|
opacity: 0.2;
|
||||||
padding: 0 16px;
|
|
||||||
line-height: $headerHeight;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 28px;
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> ul {
|
|
||||||
flex-direction: column;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
left: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform ease-out 150ms;
|
|
||||||
|
|
||||||
background-color: $headerBackground;
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
transform: translateX(0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
a, button {
|
|
||||||
.tip {
|
|
||||||
display: block;
|
|
||||||
margin-left: 8px;
|
|
||||||
text-transform: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: initial;
|
|
||||||
display: block;
|
|
||||||
padding-left: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
@media (min-width: $mobileThreshold) {
|
||||||
nav ul li {
|
flex-direction: row;
|
||||||
a, button, .button {
|
|
||||||
@include tip;
|
nav {
|
||||||
|
#menu-button {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
ul {
|
||||||
a, button, .button {
|
position: static;
|
||||||
.tip {
|
flex-direction: row;
|
||||||
left: unset;
|
transform: none;
|
||||||
right: 4px;
|
padding: 0;
|
||||||
transform: none;
|
background: transparent;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
a, button, .button {
|
||||||
|
.tip {
|
||||||
|
left: unset;
|
||||||
|
right: 4px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
background: $panelBackground;
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
|
||||||
|
box-shadow: 0 2px 2px transparentize(darken($panelBackground, 20%), 0.75);
|
||||||
|
border-top: 4px solid lighten($panelBackground, 5%);
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> li:not(:first-child) {
|
||||||
|
border-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
body > footer {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -338,7 +393,7 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($secondary, 10%);
|
color: lighten($secondary, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feather.feather-external-link {
|
.feather.feather-external-link {
|
||||||
@ -584,6 +639,10 @@ button, .button {
|
|||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background-color: $warningColor;
|
background-color: $warningColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($warningColor, 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error, &.danger {
|
&.error, &.danger {
|
||||||
@ -603,6 +662,35 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// --- Tables
|
||||||
|
// ---
|
||||||
|
td.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -614,6 +702,7 @@ button, .button {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
border-bottom: 1px solid #39434a;
|
border-bottom: 1px solid #39434a;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
@ -633,6 +722,10 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// --- Breadcrumb widget
|
||||||
|
// ---
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -646,6 +739,7 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// --- Layout helpers
|
// --- Layout helpers
|
||||||
// ---
|
// ---
|
||||||
@ -653,24 +747,6 @@ button, .button {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin container {
|
|
||||||
width: $mobileThreshold;
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
@include container;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 16px 0 48px;
|
margin: 16px 0 48px;
|
||||||
@ -692,10 +768,14 @@ button, .button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub-panel {
|
.sub-panel {
|
||||||
margin: 32px -18px;
|
margin: 32px 0;
|
||||||
padding: 1px 16px;
|
padding: 1px 16px;
|
||||||
border: 2px solid lighten($panelBackground, 4%);
|
border: 2px solid lighten($panelBackground, 4%);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
form > & {
|
||||||
|
margin: 32px -18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -719,6 +799,10 @@ button, .button {
|
|||||||
stroke-linejoin: miter;
|
stroke-linejoin: miter;
|
||||||
fill: none;
|
fill: none;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
|
h1 > &, h2 > &, h3 > & {
|
||||||
|
--icon-size: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
@ -789,37 +873,7 @@ button, .button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: hidden;
|
width: 0;
|
||||||
white-space: nowrap;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 20px;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyable-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
background-color: darken($backgroundColor, 2%);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -869,3 +923,7 @@ button, .button {
|
|||||||
background: $secondary;
|
background: $secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-col-grow {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
19
assets/sass/responsivity_tools.scss
Normal file
19
assets/sass/responsivity_tools.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
@media (min-width: $mobileThreshold) {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $desktopThreshold) {
|
||||||
|
width: $desktopThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@include container;
|
||||||
|
}
|
21
package.json
21
package.json
@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "ily.li",
|
"name": "ily.li",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"description": "Self-hosted file pusher",
|
"description": "Self-hosted file pusher",
|
||||||
"repository": "https://eternae.ink/arisu/ily.li",
|
"repository": "https://eternae.ink/arisu/ily.li",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"main": "dist/src/main.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dist-webpack": "webpack --mode production",
|
"test": "jest --verbose --runInBand",
|
||||||
"clean": "(test ! -d dist || rm -r dist)",
|
"clean": "(test ! -d dist || rm -r dist)",
|
||||||
|
"prepareSources": "cp package.json src/",
|
||||||
"compile": "yarn clean && tsc",
|
"compile": "yarn clean && tsc",
|
||||||
"build": "yarn compile && yarn dist-webpack",
|
"build": "yarn prepareSources && yarn compile && webpack --mode production",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
||||||
"dev": "concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
"start": "yarn build && node",
|
||||||
"start": "yarn build && node dist/src/main.js",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||||
"test": "jest --verbose --runInBand"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.5",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@types/config": "^0.0.36",
|
"@types/config": "^0.0.38",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
"@types/feather-icons": "^4.7.0",
|
"@types/feather-icons": "^4.7.0",
|
||||||
@ -46,6 +46,7 @@
|
|||||||
"imagemin-svgo": "^8.0.0",
|
"imagemin-svgo": "^8.0.0",
|
||||||
"img-loader": "^3.0.1",
|
"img-loader": "^3.0.1",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
|
"maildev": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^1.2.1",
|
"mini-css-extract-plugin": "^1.2.1",
|
||||||
"node-sass": "^5.0.0",
|
"node-sass": "^5.0.0",
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
@ -61,6 +62,6 @@
|
|||||||
"config": "^3.3.1",
|
"config": "^3.3.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"formidable": "^1.2.2",
|
"formidable": "^1.2.2",
|
||||||
"swaf": "^0.22.5"
|
"swaf": "^0.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
92
src/App.ts
92
src/App.ts
@ -15,34 +15,35 @@ import CsrfProtectionComponent from "swaf/components/CsrfProtectionComponent";
|
|||||||
import WebSocketServerComponent from "swaf/components/WebSocketServerComponent";
|
import WebSocketServerComponent from "swaf/components/WebSocketServerComponent";
|
||||||
import AboutController from "./controllers/AboutController";
|
import AboutController from "./controllers/AboutController";
|
||||||
import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
|
import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
|
||||||
import AuthController from "./controllers/AuthController";
|
|
||||||
import MagicLinkWebSocketListener from "swaf/auth/magic_link/MagicLinkWebSocketListener";
|
import MagicLinkWebSocketListener from "swaf/auth/magic_link/MagicLinkWebSocketListener";
|
||||||
import MagicLinkController from "./controllers/MagicLinkController";
|
|
||||||
import MailController from "swaf/auth/MailController";
|
|
||||||
import FileController from "./controllers/FileController";
|
import FileController from "./controllers/FileController";
|
||||||
import CreateUsersAndUserEmailsTable from "swaf/auth/migrations/CreateUsersAndUserEmailsTable";
|
|
||||||
import CreateMagicLinksTable from "swaf/auth/migrations/CreateMagicLinksTable";
|
|
||||||
import CreateAuthTokensTable from "./migrations/CreateAuthTokensTable";
|
import CreateAuthTokensTable from "./migrations/CreateAuthTokensTable";
|
||||||
import AuthComponent from "swaf/auth/AuthComponent";
|
import AuthComponent from "swaf/auth/AuthComponent";
|
||||||
import AuthGuard from "swaf/auth/AuthGuard";
|
|
||||||
import MagicLink from "swaf/auth/models/MagicLink";
|
|
||||||
import AuthToken from "./models/AuthToken";
|
|
||||||
import {MagicLinkActionType} from "./controllers/MagicLinkActionType";
|
|
||||||
import {Request} from "express";
|
|
||||||
import CreateFilesTable from "./migrations/CreateFilesTable";
|
import CreateFilesTable from "./migrations/CreateFilesTable";
|
||||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||||
import AddApprovedFieldToUsersTable from "swaf/auth/migrations/AddApprovedFieldToUsersTable";
|
|
||||||
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
||||||
import AuthTokenController from "./controllers/AuthTokenController";
|
import AuthTokenController from "./controllers/AuthTokenController";
|
||||||
import URLRedirectController from "./controllers/URLRedirectController";
|
import URLRedirectController from "./controllers/URLRedirectController";
|
||||||
import LinkController from "./controllers/LinkController";
|
import LinkController from "./controllers/LinkController";
|
||||||
import BackendController from "swaf/helpers/BackendController";
|
import BackendController from "swaf/helpers/BackendController";
|
||||||
import RedirectBackComponent from "swaf/components/RedirectBackComponent";
|
|
||||||
import DummyMigration from "swaf/migrations/DummyMigration";
|
import DummyMigration from "swaf/migrations/DummyMigration";
|
||||||
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
|
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
|
||||||
import {Session} from "express-session";
|
import CreateUsersAndUserEmailsTableMigration from "swaf/auth/migrations/CreateUsersAndUserEmailsTableMigration";
|
||||||
import packageJson = require('../package.json');
|
import CreateMagicLinksTableMigration from "swaf/auth/magic_link/CreateMagicLinksTableMigration";
|
||||||
import FixUserMainEmailRelation from "swaf/auth/migrations/FixUserMainEmailRelation";
|
import AddApprovedFieldToUsersTableMigration from "swaf/auth/migrations/AddApprovedFieldToUsersTableMigration";
|
||||||
|
import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
|
||||||
|
import MagicLinkAuthMethod from "swaf/auth/magic_link/MagicLinkAuthMethod";
|
||||||
|
import {MAGIC_LINK_MAIL} from "swaf/Mails";
|
||||||
|
import PasswordAuthMethod from "swaf/auth/password/PasswordAuthMethod";
|
||||||
|
import MailController from "swaf/mail/MailController";
|
||||||
|
import AccountController from "swaf/auth/AccountController";
|
||||||
|
import AuthController from "swaf/auth/AuthController";
|
||||||
|
import MagicLinkController from "swaf/auth/magic_link/MagicLinkController";
|
||||||
|
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
|
||||||
|
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
|
||||||
|
import AddPasswordToUsersMigration from "swaf/auth/password/AddPasswordToUsersMigration";
|
||||||
|
import DropNameFromUsers from "swaf/auth/migrations/DropNameFromUsers";
|
||||||
|
import packageJson = require('./package.json');
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -56,15 +57,19 @@ export default class App extends Application {
|
|||||||
return [
|
return [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
DummyMigration,
|
DummyMigration,
|
||||||
CreateUsersAndUserEmailsTable,
|
CreateUsersAndUserEmailsTableMigration,
|
||||||
CreateMagicLinksTable,
|
CreateMagicLinksTableMigration,
|
||||||
CreateAuthTokensTable,
|
CreateAuthTokensTable,
|
||||||
CreateFilesTable,
|
CreateFilesTable,
|
||||||
IncreaseFilesSizeField,
|
IncreaseFilesSizeField,
|
||||||
AddApprovedFieldToUsersTable,
|
AddApprovedFieldToUsersTableMigration,
|
||||||
CreateUrlRedirectsTable,
|
CreateUrlRedirectsTable,
|
||||||
DropLegacyLogsTable,
|
DropLegacyLogsTable,
|
||||||
FixUserMainEmailRelation,
|
DummyMigration,
|
||||||
|
AddUsedToMagicLinksMigration,
|
||||||
|
MakeMagicLinksSessionNotUniqueMigration,
|
||||||
|
AddPasswordToUsersMigration,
|
||||||
|
DropNameFromUsers,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,13 +80,8 @@ export default class App extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerComponents() {
|
private registerComponents() {
|
||||||
const redisComponent = new RedisComponent();
|
|
||||||
const mysqlComponent = new MysqlComponent();
|
|
||||||
|
|
||||||
const expressAppComponent = new ExpressAppComponent(this.addr, this.port);
|
|
||||||
this.use(expressAppComponent);
|
|
||||||
|
|
||||||
// Base
|
// Base
|
||||||
|
this.use(new ExpressAppComponent(this.addr, this.port));
|
||||||
this.use(new LogRequestsComponent());
|
this.use(new LogRequestsComponent());
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
@ -90,43 +90,22 @@ export default class App extends Application {
|
|||||||
|
|
||||||
// Dynamic views and routes
|
// Dynamic views and routes
|
||||||
this.use(new NunjucksComponent());
|
this.use(new NunjucksComponent());
|
||||||
this.use(new RedirectBackComponent());
|
this.use(new PreviousUrlComponent());
|
||||||
|
|
||||||
// Maintenance
|
// Maintenance
|
||||||
this.use(new MaintenanceComponent(this, () => {
|
this.use(new MaintenanceComponent(this, () => {
|
||||||
return redisComponent.canServe() && mysqlComponent.canServe();
|
return this.as(RedisComponent).canServe() && this.as(MysqlComponent).canServe();
|
||||||
}));
|
}));
|
||||||
this.use(new AutoUpdateComponent());
|
this.use(new AutoUpdateComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(mysqlComponent);
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent());
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(redisComponent);
|
this.use(new RedisComponent());
|
||||||
this.use(new SessionComponent(redisComponent));
|
this.use(new SessionComponent(this.as(RedisComponent)));
|
||||||
this.use(new AuthComponent(new class extends AuthGuard<MagicLink | AuthToken> {
|
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
||||||
public async getProofForSession(session: Session): Promise<MagicLink | AuthToken | null> {
|
|
||||||
return await MagicLink.bySessionId(
|
|
||||||
session.id,
|
|
||||||
[MagicLinkActionType.LOGIN, MagicLinkActionType.REGISTER],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getProofForRequest(req: Request): Promise<MagicLink | AuthToken | null> {
|
|
||||||
const authorization = req.header('Authorization');
|
|
||||||
if (authorization) {
|
|
||||||
const token = await AuthToken.select().where('secret', authorization).first();
|
|
||||||
if (token) {
|
|
||||||
token.use();
|
|
||||||
await token.save();
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await super.getProofForRequest(req);
|
|
||||||
}
|
|
||||||
}(this)));
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
this.use(new FormHelperComponent());
|
this.use(new FormHelperComponent());
|
||||||
@ -135,7 +114,7 @@ export default class App extends Application {
|
|||||||
this.use(new CsrfProtectionComponent());
|
this.use(new CsrfProtectionComponent());
|
||||||
|
|
||||||
// WebSocket server
|
// WebSocket server
|
||||||
this.use(new WebSocketServerComponent(this, expressAppComponent, redisComponent));
|
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerWebSocketListeners() {
|
private registerWebSocketListeners() {
|
||||||
@ -147,12 +126,13 @@ export default class App extends Application {
|
|||||||
this.use(new LinkController());
|
this.use(new LinkController());
|
||||||
|
|
||||||
// Priority
|
// Priority
|
||||||
|
this.use(new MailController());
|
||||||
this.use(new AuthController());
|
this.use(new AuthController());
|
||||||
this.use(new MagicLinkController(this.as<MagicLinkWebSocketListener<this>>(MagicLinkWebSocketListener)));
|
this.use(new MagicLinkController(this.as<MagicLinkWebSocketListener<this>>(MagicLinkWebSocketListener)));
|
||||||
this.use(new BackendController());
|
|
||||||
|
|
||||||
// Core functionality
|
// Core
|
||||||
this.use(new MailController());
|
this.use(new AccountController());
|
||||||
|
this.use(new BackendController());
|
||||||
|
|
||||||
// Other functionality
|
// Other functionality
|
||||||
this.use(new AuthTokenController());
|
this.use(new AuthTokenController());
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import {cryptoRandomDictionary} from "swaf/Utils";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import FileModel from "./models/FileModel";
|
import FileModel from "./models/FileModel";
|
||||||
import {ServerError} from "swaf/HttpError";
|
import {ServerError} from "swaf/HttpError";
|
||||||
|
import {nanoid} from "nanoid";
|
||||||
|
|
||||||
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
||||||
export default async function generateSlug(tries: number): Promise<string> {
|
export default async function generateSlug(tries: number): Promise<string> {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
do {
|
do {
|
||||||
const slug = cryptoRandomDictionary(config.get<number>('newlyGeneratedSlugSize'), SLUG_DICTIONARY);
|
const slug = nanoid(config.get<number>('newlyGeneratedSlugSize'));
|
||||||
if (!await FileModel.getBySlug(slug)) {
|
if (!await FileModel.getBySlug(slug)) {
|
||||||
return slug;
|
return slug;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import MagicLinkAuthController from "swaf/auth/magic_link/MagicLinkAuthController";
|
|
||||||
import {MAGIC_LINK_MAIL} from "swaf/Mails";
|
|
||||||
|
|
||||||
export default class AuthController extends MagicLinkAuthController {
|
|
||||||
public constructor() {
|
|
||||||
super(MAGIC_LINK_MAIL);
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ export default class AuthTokenController extends Controller {
|
|||||||
});
|
});
|
||||||
await authToken.save();
|
await authToken.save();
|
||||||
req.flash('success', 'Successfully created auth token.');
|
req.flash('success', 'Successfully created auth token.');
|
||||||
res.redirectBack(Controller.route('file-upload'));
|
res.redirect(req.getPreviousUrl() || Controller.route('file-upload'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postRevokeAuthToken(req: Request, res: Response): Promise<void> {
|
protected async postRevokeAuthToken(req: Request, res: Response): Promise<void> {
|
||||||
@ -34,6 +34,6 @@ export default class AuthTokenController extends Controller {
|
|||||||
await authToken.delete();
|
await authToken.delete();
|
||||||
|
|
||||||
req.flash('success', 'Successfully deleted auth token.');
|
req.flash('success', 'Successfully deleted auth token.');
|
||||||
res.redirectBack(Controller.route('file-upload'));
|
res.redirect(req.getPreviousUrl() || Controller.route('file-upload'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import * as fs from "fs";
|
|||||||
import AuthToken from "../models/AuthToken";
|
import AuthToken from "../models/AuthToken";
|
||||||
import {IncomingForm} from "formidable";
|
import {IncomingForm} from "formidable";
|
||||||
import generateSlug from "../SlugGenerator";
|
import generateSlug from "../SlugGenerator";
|
||||||
import {log} from "swaf/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
import FileUploadMiddleware from "swaf/FileUploadMiddleware";
|
import FileUploadMiddleware from "swaf/FileUploadMiddleware";
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +66,9 @@ export default class FileController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const upload = req.files['upload'];
|
const upload = req.files['upload'];
|
||||||
|
if (Array.isArray(upload)) {
|
||||||
|
throw new BadRequestError('Uploading multiple files at once is unsupported.', 'Please only upload one file at a time.', req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// TTL
|
// TTL
|
||||||
let ttl = config.get<number>('default_file_ttl');
|
let ttl = config.get<number>('default_file_ttl');
|
||||||
@ -97,7 +100,7 @@ export default class FileController extends Controller {
|
|||||||
html: () => {
|
html: () => {
|
||||||
req.flash('success', 'Upload success!');
|
req.flash('success', 'Upload success!');
|
||||||
req.flash('url', file.getURL(domain));
|
req.flash('url', file.getURL(domain));
|
||||||
res.redirectBack('/');
|
res.redirect(Controller.route('file-manager'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -132,7 +135,7 @@ export default class FileController extends Controller {
|
|||||||
text: () => res.send('success'),
|
text: () => res.send('success'),
|
||||||
html: () => {
|
html: () => {
|
||||||
req.flash('success', 'Successfully deleted file.');
|
req.flash('success', 'Successfully deleted file.');
|
||||||
res.redirectBack('/');
|
res.redirect(Controller.route('file-manager'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -140,7 +143,7 @@ export default class FileController extends Controller {
|
|||||||
public static async deleteFile(file: FileModel): Promise<void> {
|
public static async deleteFile(file: FileModel): Promise<void> {
|
||||||
fs.unlinkSync(file.getOrFail('storage_path'));
|
fs.unlinkSync(file.getOrFail('storage_path'));
|
||||||
await file.delete();
|
await file.delete();
|
||||||
log.info('Deleted', file.storage_path, `(${file.real_name})`);
|
logger.info('Deleted', file.storage_path, `(${file.real_name})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,6 @@ export default class HomeController extends Controller {
|
|||||||
* This is to test and assert that swaf extended types are available
|
* This is to test and assert that swaf extended types are available
|
||||||
*/
|
*/
|
||||||
protected async goBack(req: Request, res: Response): Promise<void> {
|
protected async goBack(req: Request, res: Response): Promise<void> {
|
||||||
res.redirectBack();
|
res.redirect(req.getPreviousUrl() || Controller.route('home'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import FileController, {FileUploadFormMiddleware} from "./FileController";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {encodeRFC5987ValueChars} from "../Utils";
|
import {encodeRFC5987ValueChars} from "../Utils";
|
||||||
import {promisify} from "util";
|
import {promisify} from "util";
|
||||||
import {log} from "swaf/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
|
|
||||||
export default class LinkController extends Controller {
|
export default class LinkController extends Controller {
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
@ -44,7 +44,7 @@ export default class LinkController extends Controller {
|
|||||||
|
|
||||||
// If file is bigger than max hotlink size, fallback to express download
|
// If file is bigger than max hotlink size, fallback to express download
|
||||||
if (stats.size > config.get<number>('max_hotlink_size') * 1024 * 1024) {
|
if (stats.size > config.get<number>('max_hotlink_size') * 1024 * 1024) {
|
||||||
log.info(`Fallback to express download for file of size ${stats.size}`);
|
logger.info(`Fallback to express download for file of size ${stats.size}`);
|
||||||
return res.download(file.getOrFail('storage_path'), fileName);
|
return res.download(file.getOrFail('storage_path'), fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export default class LinkController extends Controller {
|
|||||||
|
|
||||||
protected async domainFilter(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async domainFilter(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
if (req.hostname !== config.get('domain')) {
|
if (req.hostname !== config.get('domain')) {
|
||||||
if (req.path === '/') return res.redirect(config.get<string>('base_url'));
|
if (req.path === '/') return res.redirect(config.get<string>('public_url'));
|
||||||
throw new NotFoundHttpError('Page', req.url);
|
throw new NotFoundHttpError('Page', req.url);
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import _MagicLinkController from "swaf/auth/magic_link/MagicLinkController";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import Controller from "swaf/Controller";
|
|
||||||
import MagicLinkWebSocketListener from "swaf/auth/magic_link/MagicLinkWebSocketListener";
|
|
||||||
import MagicLink from "swaf/auth/models/MagicLink";
|
|
||||||
import AuthController from "./AuthController";
|
|
||||||
import {MagicLinkActionType} from "./MagicLinkActionType";
|
|
||||||
import App from "../App";
|
|
||||||
import AuthComponent from "swaf/auth/AuthComponent";
|
|
||||||
|
|
||||||
export default class MagicLinkController extends _MagicLinkController<App> {
|
|
||||||
public constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener<App>) {
|
|
||||||
super(magicLinkWebSocketListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void> {
|
|
||||||
switch (magicLink.action_type) {
|
|
||||||
case MagicLinkActionType.LOGIN:
|
|
||||||
case MagicLinkActionType.REGISTER: {
|
|
||||||
await AuthController.checkAndAuth(req, res, magicLink);
|
|
||||||
const proof = await this.getApp().as(AuthComponent).getAuthGuard().isAuthenticated(req.getSession());
|
|
||||||
const user = await proof?.getResource();
|
|
||||||
|
|
||||||
if (!res.headersSent && user) {
|
|
||||||
// Auth success
|
|
||||||
req.flash('success', `Authentication success. Welcome, ${user.name}!`);
|
|
||||||
res.redirect(req.query.redirect_uri?.toString() || Controller.route('home'));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,13 +8,13 @@ import AuthToken from "../models/AuthToken";
|
|||||||
|
|
||||||
export default class URLRedirectController extends Controller {
|
export default class URLRedirectController extends Controller {
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
this.get('/url/shrink', this.getURLShrinker, 'url-shrinker', RequireAuthMiddleware);
|
this.get('/url/shrink', this.getUrlShrinker, 'url-shrinker', RequireAuthMiddleware);
|
||||||
this.get('/url/shrink/script', this.downloadLinuxScript, 'url-linux-script');
|
this.get('/url/shrink/script', this.downloadLinuxScript, 'url-linux-script');
|
||||||
this.post('/url/shrink', this.addURLFrontend, 'shrink-url', RequireAuthMiddleware);
|
this.post('/url/shrink', this.addUrlFrontend, 'shrink-url', RequireAuthMiddleware);
|
||||||
this.get('/urls/:page([0-9]+)?', this.getURLRedirectManager, 'url-manager', RequireAuthMiddleware);
|
this.get('/urls/:page([0-9]+)?', this.getUrlRedirectManager, 'url-manager', RequireAuthMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getURLShrinker(req: Request, res: Response): Promise<void> {
|
protected async getUrlShrinker(req: Request, res: Response): Promise<void> {
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
||||||
res.render('url-shrinker', {
|
res.render('url-shrinker', {
|
||||||
@ -28,14 +28,14 @@ export default class URLRedirectController extends Controller {
|
|||||||
res.download('assets/files/shrink_url.sh', 'shrink_url.sh');
|
res.download('assets/files/shrink_url.sh', 'shrink_url.sh');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getURLRedirectManager(req: Request, res: Response): Promise<void> {
|
protected async getUrlRedirectManager(req: Request, res: Response): Promise<void> {
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
res.render('url-manager', {
|
res.render('url-manager', {
|
||||||
urls: await URLRedirect.paginateForUser(req, 100, user.getOrFail('id')),
|
urls: await URLRedirect.paginateForUser(req, 100, user.getOrFail('id')),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async addURLFrontend(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async addUrlFrontend(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
req.body.type = 'url';
|
req.body.type = 'url';
|
||||||
await URLRedirectController.addURL(
|
await URLRedirectController.addURL(
|
||||||
req,
|
req,
|
||||||
@ -75,7 +75,7 @@ export default class URLRedirectController extends Controller {
|
|||||||
html: () => {
|
html: () => {
|
||||||
req.flash('success', 'URL shrunk successfully!');
|
req.flash('success', 'URL shrunk successfully!');
|
||||||
req.flash('url', urlRedirect.getURL(domain));
|
req.flash('url', urlRedirect.getURL(domain));
|
||||||
res.redirectBack('/');
|
res.redirect(Controller.route('url-manager'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
10
src/main.ts
10
src/main.ts
@ -2,19 +2,19 @@ import {delimiter} from "path";
|
|||||||
|
|
||||||
// Load config from specified path or default + swaf/config (default defaults)
|
// Load config from specified path or default + swaf/config (default defaults)
|
||||||
process.env['NODE_CONFIG_DIR'] =
|
process.env['NODE_CONFIG_DIR'] =
|
||||||
__dirname + '/../../node_modules/swaf/config/'
|
__dirname + '/../node_modules/swaf/config/'
|
||||||
+ delimiter
|
+ delimiter
|
||||||
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../../config/');
|
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../config/');
|
||||||
|
|
||||||
import {log} from "swaf/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
||||||
|
|
||||||
const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
|
const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
|
||||||
await app.start();
|
await app.start();
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
log.error(err);
|
logger.error(err);
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import {Connection} from "mysql";
|
|
||||||
import Migration from "swaf/db/Migration";
|
import Migration from "swaf/db/Migration";
|
||||||
import ModelFactory from "swaf/db/ModelFactory";
|
import ModelFactory from "swaf/db/ModelFactory";
|
||||||
import AuthToken from "../models/AuthToken";
|
import AuthToken from "../models/AuthToken";
|
||||||
|
|
||||||
export default class CreateAuthTokensTable extends Migration {
|
export default class CreateAuthTokensTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`CREATE TABLE auth_tokens
|
await this.query(`CREATE TABLE auth_tokens
|
||||||
(
|
(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
@ -14,11 +13,11 @@ export default class CreateAuthTokensTable extends Migration {
|
|||||||
used_at DATETIME NOT NULL DEFAULT NOW(),
|
used_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
ttl INT UNSIGNED NOT NULL,
|
ttl INT UNSIGNED NOT NULL,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
)`, connection);
|
)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query(`DROP TABLE IF EXISTS auth_tokens`, connection);
|
await this.query(`DROP TABLE IF EXISTS auth_tokens`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import {Connection} from "mysql";
|
|
||||||
import Migration from "swaf/db/Migration";
|
import Migration from "swaf/db/Migration";
|
||||||
import ModelFactory from "swaf/db/ModelFactory";
|
import ModelFactory from "swaf/db/ModelFactory";
|
||||||
import FileModel from "../models/FileModel";
|
import FileModel from "../models/FileModel";
|
||||||
|
|
||||||
export default class CreateFilesTable extends Migration {
|
export default class CreateFilesTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`CREATE TABLE files
|
await this.query(`CREATE TABLE files
|
||||||
(
|
(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
@ -17,11 +16,11 @@ export default class CreateFilesTable extends Migration {
|
|||||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
ttl INT UNSIGNED NOT NULL,
|
ttl INT UNSIGNED NOT NULL,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
)`, connection);
|
)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query(`DROP TABLE IF EXISTS files`, connection);
|
await this.query(`DROP TABLE IF EXISTS files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import Migration from "swaf/db/Migration";
|
import Migration from "swaf/db/Migration";
|
||||||
import {Connection} from "mysql";
|
|
||||||
import ModelFactory from "swaf/db/ModelFactory";
|
import ModelFactory from "swaf/db/ModelFactory";
|
||||||
import URLRedirect from "../models/URLRedirect";
|
import URLRedirect from "../models/URLRedirect";
|
||||||
|
|
||||||
export default class CreateUrlRedirectsTable extends Migration {
|
export default class CreateUrlRedirectsTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`CREATE TABLE url_redirects
|
await this.query(`CREATE TABLE url_redirects
|
||||||
(
|
(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
@ -13,11 +12,11 @@ export default class CreateUrlRedirectsTable extends Migration {
|
|||||||
target_url VARCHAR(1745) NOT NULL,
|
target_url VARCHAR(1745) NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
)`, connection);
|
)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query(`DROP TABLE IF EXISTS url_redirects`, connection);
|
await this.query(`DROP TABLE IF EXISTS url_redirects`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import Migration from "swaf/db/Migration";
|
import Migration from "swaf/db/Migration";
|
||||||
import {Connection} from "mysql";
|
|
||||||
|
|
||||||
export default class IncreaseFilesSizeField extends Migration {
|
export default class IncreaseFilesSizeField extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE files MODIFY size BIGINT UNSIGNED`, connection);
|
await this.query(`ALTER TABLE files MODIFY size BIGINT UNSIGNED`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE files MODIFY size INT UNSIGNED`, connection);
|
await this.query(`ALTER TABLE files MODIFY size INT UNSIGNED`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Model from "swaf/db/Model";
|
import Model from "swaf/db/Model";
|
||||||
import AuthProof from "swaf/auth/AuthProof";
|
import AuthProof from "swaf/auth/AuthProof";
|
||||||
import User from "swaf/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {cryptoRandomDictionary} from "swaf/Utils";
|
import {nanoid} from "nanoid";
|
||||||
|
|
||||||
export default class AuthToken extends Model implements AuthProof<User> {
|
export default class AuthToken extends Model implements AuthProof<User> {
|
||||||
public id?: number = undefined;
|
public id?: number = undefined;
|
||||||
@ -19,7 +19,7 @@ export default class AuthToken extends Model implements AuthProof<User> {
|
|||||||
|
|
||||||
protected async autoFill(): Promise<void> {
|
protected async autoFill(): Promise<void> {
|
||||||
if (!this.secret) {
|
if (!this.secret) {
|
||||||
this.secret = cryptoRandomDictionary(64, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_');
|
this.secret = nanoid(64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export default class FileModel extends Model {
|
|||||||
this.setValidation('ttl').defined().min(0).max(4294967295);
|
this.setValidation('ttl').defined().min(0).max(4294967295);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
public getURL(domain: string = config.get<string>('public_url')): string {
|
||||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-file', {
|
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-file', {
|
||||||
slug: this.getOrFail('slug'),
|
slug: this.getOrFail('slug'),
|
||||||
});
|
});
|
||||||
|
@ -30,7 +30,7 @@ export default class URLRedirect extends Model {
|
|||||||
this.setValidation('target_url').defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i);
|
this.setValidation('target_url').defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
public getURL(domain: string = config.get<string>('public_url')): string {
|
||||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-url', {
|
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-url', {
|
||||||
slug: this.getOrFail('slug'),
|
slug: this.getOrFail('slug'),
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "public/js",
|
"outDir": "public/js",
|
||||||
|
"rootDir": "./assets",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
"rootDir": "./src",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Setup a desktop utility</h2>
|
<h2><i data-feather="tool"></i> Setup a desktop utility</h2>
|
||||||
<p>There may be a desktop client at some point. For now, if you're an advanced user, you can setup
|
<p>There may be a desktop client at some point. For now, if you're an advanced user, you can setup
|
||||||
scripts/macros.</p>
|
scripts/macros.</p>
|
||||||
|
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th class="table-col-grow">Name</th>
|
||||||
<th>Download link</th>
|
<th>Download link</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -114,46 +114,46 @@
|
|||||||
<p>For examples with curl, please download and review the scripts above.</p>
|
<p>For examples with curl, please download and review the scripts above.</p>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Auth tokens</h2>
|
<h2><i data-feather="key"></i> Auth tokens</h2>
|
||||||
<form action="{{ route('generate-token') }}" method="POST">
|
<form action="{{ route('generate-token') }}" method="POST">
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
|
||||||
<button type="submit"><i data-feather="plus"></i> Generate a new token</button>
|
<button type="submit"><i data-feather="plus"></i> Generate a new token</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Secret</th>
|
|
||||||
<th>Created at</th>
|
|
||||||
<th>Last used at</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for token in auth_tokens %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ token.id }}</td>
|
<th>#</th>
|
||||||
<td>
|
<th class="table-col-grow">Secret</th>
|
||||||
<div class="copyable-text">
|
<th>Created at</th>
|
||||||
<div class="content">{{ token.secret }}</div>
|
<th>Last used at</th>
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
<th>Actions</th>
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ token.created_at.toISOString() }}</td>
|
|
||||||
<td>{{ token.used_at.toISOString() }}</td>
|
|
||||||
<td>
|
|
||||||
<form action="{{ route('revoke-token', token.id) }}" method="POST">
|
|
||||||
<button class="button danger"><i data-feather="trash"></i> Revoke</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
|
||||||
</table>
|
<tbody>
|
||||||
</section>
|
{% for token in auth_tokens %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ token.id }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="copyable-text">
|
||||||
|
<div class="content">{{ token.secret }}</div>
|
||||||
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ token.created_at.toISOString() }}</td>
|
||||||
|
<td>{{ token.used_at.toISOString() }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form action="{{ route('revoke-token', token.id) }}" method="POST">
|
||||||
|
<button class="button danger"><i data-feather="trash"></i> <span class="tip">Revoke</span></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
@ -10,47 +10,49 @@
|
|||||||
<h1>File manager</h1>
|
<h1>File manager</h1>
|
||||||
<p>You're their manager, please be nice with them.</p>
|
<p>You're their manager, please be nice with them.</p>
|
||||||
|
|
||||||
<section class="panel">
|
<div class="container">
|
||||||
<h2>File list</h2>
|
<section class="panel">
|
||||||
<table class="data-table">
|
<h2><i data-feather="folder"></i> File list</h2>
|
||||||
<thead>
|
<table class="data-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>#</th>
|
|
||||||
<th>URL</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Expires at</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for file in files %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ file.id }}</td>
|
<th>#</th>
|
||||||
<td>
|
<th class="table-col-grow">URL</th>
|
||||||
<div class="copyable-text">
|
<th>Name</th>
|
||||||
<a class="content" href="{{ file.getURL() }}" target="_blank">{{ file.getURL() }}</a>
|
<th>Size</th>
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
<th>Expires at</th>
|
||||||
</div>
|
<th>Actions</th>
|
||||||
</td>
|
|
||||||
<td>{{ file.real_name }}</td>
|
|
||||||
<td>{{ (file.size / (1024 * 1024)).toFixed(2) }}MB</td>
|
|
||||||
{% set expires_at = file.getExpirationDate() %}
|
|
||||||
<td>{% if expires_at %}{{ expires_at.toISOString() }}{% else %}Never{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
{% if file.shouldBeDeleted() %}
|
|
||||||
Pending deletion
|
|
||||||
{% else %}
|
|
||||||
<form action="{{ route('delete-file-frontend', file.slug) }}" method="post">
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
<button class="button danger"><i data-feather="trash"></i> Delete</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
|
||||||
</table>
|
<tbody>
|
||||||
</section>
|
{% for file in files %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ file.id }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="copyable-text">
|
||||||
|
<a class="content" href="{{ file.getURL() }}" target="_blank">{{ file.getURL() }}</a>
|
||||||
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><pre>{{ file.real_name }}</pre></td>
|
||||||
|
<td>{{ (file.size / (1024 * 1024)).toFixed(2) }}MB</td>
|
||||||
|
{% set expires_at = file.getExpirationDate() %}
|
||||||
|
<td>{% if expires_at %}{{ expires_at.toISOString() }}{% else %}Never{% endif %}</td>
|
||||||
|
<td class="actions">
|
||||||
|
{% if file.shouldBeDeleted() %}
|
||||||
|
Pending deletion
|
||||||
|
{% else %}
|
||||||
|
<form action="{{ route('delete-file-frontend', file.slug) }}" method="post">
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
<button class="button danger"><i data-feather="trash"></i> <span class="tip">Delete</span></button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Upload a file</h2>
|
<h2><i data-feather="upload"></i> Upload a file</h2>
|
||||||
|
|
||||||
<form action="{{ route('post-file-frontend') }}" method="POST" enctype="multipart/form-data"
|
<form action="{{ route('post-file-frontend') }}" method="POST" enctype="multipart/form-data"
|
||||||
id="upload-form">
|
id="upload-form">
|
||||||
|
@ -18,19 +18,30 @@
|
|||||||
<ul id="main-menu">
|
<ul id="main-menu">
|
||||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> <span class="tip">About</span></a></li>
|
<li><a href="{{ route('about') }}"><i data-feather="info"></i> <span class="tip">About</span></a></li>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<li><a href="{{ route('file-manager') }}"><i data-feather="folder"></i> <span class="tip">File manager</span></a></li>
|
<li><a href="{{ route('file-manager') }}"><i data-feather="folder"></i> <span class="tip">File manager</span></a>
|
||||||
<li><a href="{{ route('file-upload') }}"><i data-feather="upload"></i> <span class="tip">Upload file</span></a></li>
|
<ul class="dropdown">
|
||||||
<li><a href="{{ route('url-manager') }}"><i data-feather="link"></i> <span class="tip">URL manager</span></a></li>
|
<li><a href="{{ route('file-upload') }}"><i data-feather="upload"></i> <span class="tip">Upload file</span></a></li>
|
||||||
<li><a href="{{ route('url-shrinker') }}"><i data-feather="crosshair"></i> <span class="tip">Shrink URL</span></a></li>
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="{{ route('url-manager') }}"><i data-feather="link"></i> <span class="tip">URL manager</span></a>
|
||||||
|
<ul class="dropdown">
|
||||||
|
<li><a href="{{ route('url-shrinker') }}"><i data-feather="crosshair"></i> <span class="tip">Shrink URL</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li><a href="{{ route('account') }}"><i data-feather="user"></i> <span class="tip">{{ user.name | default('Account') }}</span></a>
|
||||||
<form action="{{ route('logout') }}?{{ querystring.stringify({redirect_uri: '/'}) }}" method="POST">
|
<ul class="dropdown">
|
||||||
<button><i data-feather="log-out"></i> <span class="tip">Logout</span></button>
|
<li>
|
||||||
|
<hr>
|
||||||
|
<form action="{{ route('logout') }}?{{ querystring.stringify({redirect_uri: '/'}) }}" method="POST">
|
||||||
|
<button><i data-feather="log-out"></i> <span class="tip">Logout</span></button>
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ route('auth') }}"><i data-feather="user"></i> <span class="tip">Login / Register</span></a></li>
|
<li><a href="{{ route('auth') }}"><i data-feather="user"></i> <span class="tip">Login / Register</span></a></li>
|
||||||
|
@ -8,31 +8,33 @@
|
|||||||
<h1>URL manager</h1>
|
<h1>URL manager</h1>
|
||||||
<p>These are permanent.</p>
|
<p>These are permanent.</p>
|
||||||
|
|
||||||
<section class="panel">
|
<div class="container">
|
||||||
<h2>URL list</h2>
|
<section class="panel">
|
||||||
<table class="data-table">
|
<h2><i data-feather="link"></i> URL list</h2>
|
||||||
<thead>
|
<table class="data-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>#</th>
|
|
||||||
<th>URL</th>
|
|
||||||
<th>Target</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for url in urls %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ url.id }}</td>
|
<th>#</th>
|
||||||
<td>
|
<th class="table-col-grow">URL</th>
|
||||||
<div class="copyable-text">
|
<th>Target</th>
|
||||||
<a class="content" href="{{ url.getURL() }}" target="_blank">{{ url.getURL() }}</a>
|
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ url.target_url }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
|
||||||
</table>
|
<tbody>
|
||||||
</section>
|
{% for url in urls %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ url.id }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="copyable-text">
|
||||||
|
<a class="content" href="{{ url.getURL() }}" target="_blank">{{ url.getURL() }}</a>
|
||||||
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><pre>{{ url.target_url }}</pre></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Shrink a URL</h2>
|
<h2><i data-feather="crosshair"></i> Shrink a URL</h2>
|
||||||
|
|
||||||
<form action="{{ route('shrink-url') }}" method="POST" id="url-shrink-form">
|
<form action="{{ route('shrink-url') }}" method="POST" id="url-shrink-form">
|
||||||
{{ macros.field(_locals, 'text', 'target_url', '', 'Target URL', 'Only valid URLs starting with http:// or https://', validation_attributes='required') }}
|
{{ macros.field(_locals, 'text', 'target_url', '', 'Target URL', 'Only valid URLs starting with http:// or https://', validation_attributes='required') }}
|
||||||
|
Loading…
Reference in New Issue
Block a user