Merge branch 'develop'
This commit is contained in:
commit
5016b46629
@ -2,6 +2,7 @@
|
|||||||
"bundles": {
|
"bundles": {
|
||||||
"app": "js/app.js",
|
"app": "js/app.js",
|
||||||
"fm": "js/fm.js",
|
"fm": "js/fm.js",
|
||||||
|
"url-shrinker": "js/url-shrinker.js",
|
||||||
"layout": "sass/layout.scss",
|
"layout": "sass/layout.scss",
|
||||||
"error": "sass/error.scss",
|
"error": "sass/error.scss",
|
||||||
"logo": "img/logo.svg",
|
"logo": "img/logo.svg",
|
||||||
|
17
assets/files/shrink_url.sh
Executable file
17
assets/files/shrink_url.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
target_url=$1
|
||||||
|
slug=$2
|
||||||
|
token=$(cat "$HOME/.ily_token")
|
||||||
|
|
||||||
|
if test -z "$slug"; then
|
||||||
|
method=POST
|
||||||
|
else
|
||||||
|
method=PUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -X $method -H 'Accept: text/plain' \
|
||||||
|
-H "Authorization: $token" \
|
||||||
|
-F 'type=url' \
|
||||||
|
-F "target_url=$target_url" \
|
||||||
|
"https://ily.li/$slug"
|
@ -1,18 +1,18 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
local_file=$1
|
local_file=$1
|
||||||
token=$(cat $HOME/.ily_token)
|
slug=$2
|
||||||
|
token=$(cat "$HOME/.ily_token")
|
||||||
|
|
||||||
if test -z "$2"; then
|
if test -z "$slug"; then
|
||||||
curl -X POST -H 'Accept: text/plain' \
|
method=POST
|
||||||
-H "Authorization: $token" \
|
|
||||||
-F 'upload=@'$local_file \
|
|
||||||
-F 'ttl=2592000' \
|
|
||||||
https://ily.li/
|
|
||||||
else
|
else
|
||||||
curl -X PUT -H 'Accept: text/plain' \
|
method=PUT
|
||||||
-H "Authorization: $token" \
|
|
||||||
-F 'upload=@'$local_file \
|
|
||||||
-F 'ttl=2592000' \
|
|
||||||
"https://ily.li/$2"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
curl -X $method -H 'Accept: text/plain' \
|
||||||
|
-H "Authorization: $token" \
|
||||||
|
-F 'type=file' \
|
||||||
|
-F "upload=@$local_file" \
|
||||||
|
-F 'ttl=2592000' \
|
||||||
|
"https://ily.li/$slug"
|
||||||
|
@ -2,6 +2,7 @@ import './external_links';
|
|||||||
import './message_icons';
|
import './message_icons';
|
||||||
import './forms';
|
import './forms';
|
||||||
import './copyable_text';
|
import './copyable_text';
|
||||||
|
import './main_menu';
|
||||||
|
|
||||||
import '../sass/app.scss';
|
import '../sass/app.scss';
|
||||||
|
|
||||||
|
17
assets/js/main_menu.js
Normal file
17
assets/js/main_menu.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const menuButton = document.getElementById('menu-button');
|
||||||
|
const mainMenu = document.getElementById('main-menu');
|
||||||
|
|
||||||
|
menuButton.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
mainMenu.classList.toggle('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
mainMenu.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
mainMenu.classList.remove('open');
|
||||||
|
});
|
||||||
|
});
|
11
assets/js/url-shrinker.js
Normal file
11
assets/js/url-shrinker.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('url-shrink-form');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const autogenUrlCheckbox = document.getElementById('field-autogen_url');
|
||||||
|
const slugField = document.getElementById('field-slug');
|
||||||
|
|
||||||
|
autogenUrlCheckbox.addEventListener('change', () => {
|
||||||
|
slugField.disabled = autogenUrlCheckbox.checked;
|
||||||
|
});
|
||||||
|
});
|
@ -25,3 +25,6 @@ $warningColor: desaturate($warningText, 50%);
|
|||||||
$error: #ff0000;
|
$error: #ff0000;
|
||||||
$errorText: darken($error, 30%);
|
$errorText: darken($error, 30%);
|
||||||
$errorColor: desaturate($errorText, 50%);
|
$errorColor: desaturate($errorText, 50%);
|
||||||
|
|
||||||
|
// Responsivity
|
||||||
|
$menuLayoutSwitchTreshold: 700px;
|
||||||
|
@ -51,32 +51,132 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul {
|
nav {
|
||||||
display: flex;
|
ul {
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
margin: 0;
|
flex-direction: row;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
a, span {
|
a, span {
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: row;
|
||||||
height: 64px;
|
align-items: center;
|
||||||
padding: 0 24px;
|
height: 64px;
|
||||||
|
padding: 0 24px;
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $menuLayoutSwitchTreshold) {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 16px 0 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
#menu-button {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
line-height: $headerHeight;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
--icon-size: 24px;
|
--icon-size: 28px;
|
||||||
margin-right: 10px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
ul {
|
||||||
background-color: rgba(255, 255, 255, 0.07);
|
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 {
|
||||||
|
.tip {
|
||||||
|
display: block;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $menuLayoutSwitchTreshold) {
|
||||||
|
nav ul li {
|
||||||
|
a .tip {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
line-height: 22px;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: $defaultTextColor;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity ease-out 100ms, visibility step-end 150ms;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child a .tip {
|
||||||
|
left: unset;
|
||||||
|
right: 4px;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,4 +33,10 @@ export default Object.assign(require("wms-core/config/default").default, {
|
|||||||
from: 'contact@ily.li',
|
from: 'contact@ily.li',
|
||||||
from_name: 'ily.li',
|
from_name: 'ily.li',
|
||||||
},
|
},
|
||||||
|
allowed_url_domains: [
|
||||||
|
'localhost:4893',
|
||||||
|
'127.0.0.1:4893',
|
||||||
|
],
|
||||||
|
default_url_domain_for_files: 0,
|
||||||
|
default_url_domain_for_urls: 1,
|
||||||
});
|
});
|
@ -21,4 +21,10 @@ export default Object.assign(require("wms-core/config/production").default, {
|
|||||||
secure: true,
|
secure: true,
|
||||||
allow_invalid_tls: false
|
allow_invalid_tls: false
|
||||||
},
|
},
|
||||||
|
allowed_url_domains: [
|
||||||
|
'ily.li',
|
||||||
|
'gris.li',
|
||||||
|
],
|
||||||
|
default_url_domain_for_files: 0,
|
||||||
|
default_url_domain_for_urls: 1,
|
||||||
});
|
});
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ily.li",
|
"name": "ily.li",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"description": "Self-hosted file pusher",
|
"description": "Self-hosted file pusher",
|
||||||
"repository": "git@gitlab.com:ArisuOngaku/ily.li.git",
|
"repository": "git@gitlab.com:ArisuOngaku/ily.li.git",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
|
12
src/App.ts
12
src/App.ts
@ -36,6 +36,10 @@ import CreateFilesTable from "./migrations/CreateFilesTable";
|
|||||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||||
import BackendController from "./controllers/BackendController";
|
import BackendController from "./controllers/BackendController";
|
||||||
|
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
||||||
|
import AuthTokenController from "./controllers/AuthTokenController";
|
||||||
|
import URLRedirectController from "./controllers/URLRedirectController";
|
||||||
|
import LinkController from "./controllers/LinkController";
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
@ -56,6 +60,7 @@ export default class App extends Application {
|
|||||||
CreateFilesTable,
|
CreateFilesTable,
|
||||||
IncreaseFilesSizeField,
|
IncreaseFilesSizeField,
|
||||||
AddApprovedFieldToUsersTable,
|
AddApprovedFieldToUsersTable,
|
||||||
|
CreateUrlRedirectsTable,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +133,9 @@ export default class App extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerControllers() {
|
private registerControllers() {
|
||||||
|
// Multi-domain + vhost
|
||||||
|
this.use(new LinkController());
|
||||||
|
|
||||||
// Priority
|
// Priority
|
||||||
this.use(new AuthController());
|
this.use(new AuthController());
|
||||||
this.use(new MagicLinkController(this.magicLinkWebSocketListener!));
|
this.use(new MagicLinkController(this.magicLinkWebSocketListener!));
|
||||||
@ -136,10 +144,14 @@ export default class App extends Application {
|
|||||||
// Core functionality
|
// Core functionality
|
||||||
this.use(new MailController());
|
this.use(new MailController());
|
||||||
|
|
||||||
|
// Other functionality
|
||||||
|
this.use(new AuthTokenController());
|
||||||
|
|
||||||
// Semi-static
|
// Semi-static
|
||||||
this.use(new AboutController());
|
this.use(new AboutController());
|
||||||
|
|
||||||
// Global slug
|
// Global slug
|
||||||
this.use(new FileController());
|
this.use(new FileController());
|
||||||
|
this.use(new URLRedirectController());
|
||||||
}
|
}
|
||||||
}
|
}
|
17
src/SlugGenerator.ts
Normal file
17
src/SlugGenerator.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||||
|
import config from "config";
|
||||||
|
import FileModel from "./models/FileModel";
|
||||||
|
import {ServerError} from "wms-core/HttpError";
|
||||||
|
|
||||||
|
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
export default async function generateSlug(tries: number): Promise<string> {
|
||||||
|
let i = 0;
|
||||||
|
do {
|
||||||
|
const slug = cryptoRandomDictionary(config.get<number>('newlyGeneratedSlugSize'), SLUG_DICTIONARY);
|
||||||
|
if (!await FileModel.getBySlug(slug)) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} while (i < tries);
|
||||||
|
throw new ServerError('Failed to generate slug; newly generated slug size should be increased by 1.');
|
||||||
|
};
|
36
src/controllers/AuthTokenController.ts
Normal file
36
src/controllers/AuthTokenController.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import AuthToken from "../models/AuthToken";
|
||||||
|
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "wms-core/HttpError";
|
||||||
|
|
||||||
|
export default class AuthTokenController extends Controller {
|
||||||
|
routes(): void {
|
||||||
|
this.post('/gen-auth-token', this.postGenAuthToken, 'generate-token', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
this.post('/revoke-auth-token/:id', this.postRevokeAuthToken, 'revoke-token', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postGenAuthToken(req: Request, res: Response): Promise<void> {
|
||||||
|
const authToken = new AuthToken({
|
||||||
|
user_id: req.models.user!.id,
|
||||||
|
ttl: req.body.ttl ? parseInt(req.body.ttl) : 365 * 24 * 3600,
|
||||||
|
});
|
||||||
|
await authToken.save();
|
||||||
|
req.flash('success', 'Successfully created auth token.');
|
||||||
|
res.redirectBack(Controller.route('file-upload'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postRevokeAuthToken(req: Request, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) throw new BadRequestError('Cannot revoke token without an id.', 'Please provide an id.', req.url);
|
||||||
|
|
||||||
|
const authToken = await AuthToken.getById<AuthToken>(parseInt(id));
|
||||||
|
if (!authToken) throw new NotFoundHttpError('Auth token', req.url);
|
||||||
|
if (!authToken.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('auth token', req.url);
|
||||||
|
|
||||||
|
await authToken.delete();
|
||||||
|
|
||||||
|
req.flash('success', 'Successfully deleted auth token.');
|
||||||
|
res.redirectBack(Controller.route('file-upload'));
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +1,33 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "wms-core/Controller";
|
||||||
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
import {Request, Response} from "express";
|
import {NextFunction, Request, Response} from "express";
|
||||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
import {BadRequestError, ForbiddenHttpError, ServerError} from "wms-core/HttpError";
|
||||||
import FileModel from "../models/FileModel";
|
import FileModel from "../models/FileModel";
|
||||||
import {cryptoRandomDictionary} from "wms-core/Utils";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import AuthToken from "../models/AuthToken";
|
import AuthToken from "../models/AuthToken";
|
||||||
import {IncomingForm} from "formidable";
|
import {IncomingForm} from "formidable";
|
||||||
import {FILE_UPLOAD_MIDDLEWARE} from "wms-core/components/ExpressAppComponent";
|
import {FILE_UPLOAD_MIDDLEWARE} from "wms-core/components/ExpressAppComponent";
|
||||||
|
import Logger from "wms-core/Logger";
|
||||||
|
import generateSlug from "../SlugGenerator";
|
||||||
|
|
||||||
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
||||||
|
|
||||||
export default class FileController extends Controller {
|
export default class FileController extends Controller {
|
||||||
routes(): void {
|
routes(): void {
|
||||||
this.get('/files/upload', this.getFileUploader, 'file-upload', REQUIRE_AUTH_MIDDLEWARE);
|
this.get('/files/upload', this.getFileUploader, 'file-upload', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
this.get('/files/upload/script', this.downloadLinuxScript, 'linux-script');
|
this.get('/files/upload/script', this.downloadLinuxScript, 'file-linux-script');
|
||||||
this.post('/files/post', this.postFileFrontend, 'post-file-frontend', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
this.post('/files/post', this.postFileFrontend, 'post-file-frontend', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||||
this.get('/files/:page([0-9]+)?', this.getFileManager, 'file-manager', REQUIRE_AUTH_MIDDLEWARE);
|
this.get('/files/:page([0-9]+)?', this.getFileManager, 'file-manager', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
this.get('/files/delete/:slug', this.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
|
this.post('/files/delete/:slug', FileController.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
this.post('/gen-auth-token', this.postGenAuthToken, 'generate-token', REQUIRE_AUTH_MIDDLEWARE);
|
|
||||||
this.get('/revoke-auth-token/:id', this.getRevokeAuthToken, 'revoke-token', REQUIRE_AUTH_MIDDLEWARE);
|
|
||||||
|
|
||||||
this.post('/', this.postFile, 'post-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
|
||||||
this.delete('/delete/:slug', this.deleteFile, 'delete-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
|
||||||
this.get('/:slug', this.downloadFile, 'get-file');
|
|
||||||
this.put('/:slug', this.putFile, 'put-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
||||||
|
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
||||||
res.render('file-upload', {
|
res.render('file-upload', {
|
||||||
max_upload_size: config.get<string>('max_upload_size'),
|
max_upload_size: config.get<string>('max_upload_size'),
|
||||||
auth_tokens: await AuthToken.select().where('user_id', req.models.user!.id!),
|
auth_tokens: await AuthToken.select().where('user_id', req.models.user!.id!),
|
||||||
|
allowed_domains: allowedDomains,
|
||||||
|
default_domain: allowedDomains[config.get<number>('default_url_domain_for_files')],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,59 +41,12 @@ export default class FileController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postGenAuthToken(req: Request, res: Response): Promise<void> {
|
|
||||||
const authToken = new AuthToken({
|
|
||||||
user_id: req.models.user!.id,
|
|
||||||
ttl: req.body.ttl ? parseInt(req.body.ttl) : 365 * 24 * 3600,
|
|
||||||
});
|
|
||||||
await authToken.save();
|
|
||||||
req.flash('success', 'Successfully created auth token.');
|
|
||||||
res.redirectBack(Controller.route('file-upload'));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRevokeAuthToken(req: Request, res: Response): Promise<void> {
|
|
||||||
const id = req.params.id;
|
|
||||||
if (!id) throw new BadRequestError('Cannot revoke token without an id.', 'Please provide an id.', req.url);
|
|
||||||
|
|
||||||
const authToken = await AuthToken.getById<AuthToken>(parseInt(id));
|
|
||||||
if (!authToken) throw new NotFoundHttpError('Auth token', req.url);
|
|
||||||
if (!authToken.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('auth token', req.url);
|
|
||||||
|
|
||||||
await authToken.delete();
|
|
||||||
|
|
||||||
req.flash('success', 'Successfully deleted auth token.');
|
|
||||||
res.redirectBack(Controller.route('file-upload'));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postFileFrontend(req: Request, res: Response): Promise<void> {
|
protected async postFileFrontend(req: Request, res: Response): Promise<void> {
|
||||||
await this.handleFileUpload(req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await this.generateSlug(10), req, res);
|
req.body.type = 'file';
|
||||||
|
await FileController.handleFileUpload(req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await generateSlug(10), req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async downloadFile(req: Request, res: Response): Promise<void> {
|
public static async handleFileUpload(slug: string, req: Request, res: Response): Promise<void> {
|
||||||
const file = await FileModel.getBySlug(req.params.slug);
|
|
||||||
if (!file || file.shouldBeDeleted()) throw new NotFoundHttpError('File', req.url);
|
|
||||||
|
|
||||||
switch (file.storage_type) {
|
|
||||||
case 'local':
|
|
||||||
res.download(file.storage_path, file.real_name);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ServerError(`This file cannot be served. Download protocol for ${file.storage_type} storage type not implemented.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postFile(req: Request, res: Response): Promise<void> {
|
|
||||||
await this.handleFileUpload(req.body.slug || await this.generateSlug(10), req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async putFile(req: Request, res: Response): Promise<void> {
|
|
||||||
const slug = req.params.slug;
|
|
||||||
if (!slug) throw new BadRequestError('Cannot put without a slug.', 'Either provide a slug or use POST method instead.', req.url);
|
|
||||||
|
|
||||||
await this.handleFileUpload(slug, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleFileUpload(slug: string, req: Request, res: Response): Promise<void> {
|
|
||||||
// Check for file upload
|
// Check for file upload
|
||||||
if (!req.files || !req.files['upload']) {
|
if (!req.files || !req.files['upload']) {
|
||||||
throw new BadRequestError('No file received.', 'You must upload exactly one (1) file.', req.url);
|
throw new BadRequestError('No file received.', 'You must upload exactly one (1) file.', req.url);
|
||||||
@ -124,24 +73,26 @@ export default class FileController extends Controller {
|
|||||||
await file.save();
|
await file.save();
|
||||||
fs.renameSync(upload.path, file.storage_path);
|
fs.renameSync(upload.path, file.storage_path);
|
||||||
|
|
||||||
|
const domain = req.body.url_domain || config.get<string[]>('allowed_url_domains')[config.get<number>('default_url_domain_for_files')];
|
||||||
res.format({
|
res.format({
|
||||||
json: () => res.json({
|
json: () => res.json({
|
||||||
url: file.getURL(),
|
url: file.getURL(domain),
|
||||||
}),
|
}),
|
||||||
text: () => res.send(file.getURL()),
|
text: () => res.send(file.getURL(domain)),
|
||||||
html: () => {
|
html: () => {
|
||||||
req.flash('success', 'Upload success!');
|
req.flash('success', 'Upload success!');
|
||||||
|
req.flash('url', file.getURL(domain));
|
||||||
res.redirectBack('/');
|
res.redirectBack('/');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async deleteFile(req: Request, res: Response): Promise<void> {
|
public static async deleteFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
const slug = req.params.slug;
|
const slug = req.params.slug;
|
||||||
if (!slug) throw new BadRequestError('Cannot delete nothing.', 'Please provide a slug.', req.url);
|
if (!slug) throw new BadRequestError('Cannot delete nothing.', 'Please provide a slug.', req.url);
|
||||||
|
|
||||||
const file = await FileModel.getBySlug(req.params.slug);
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
if (!file) throw new NotFoundHttpError('File', req.url);
|
if (!file) return next();
|
||||||
if (!file.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('file', req.url);
|
if (!file.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('file', req.url);
|
||||||
|
|
||||||
switch (file.storage_type) {
|
switch (file.storage_type) {
|
||||||
@ -164,22 +115,10 @@ export default class FileController extends Controller {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateSlug(tries: number): Promise<string> {
|
|
||||||
let i = 0;
|
|
||||||
do {
|
|
||||||
const slug = cryptoRandomDictionary(config.get<number>('newlyGeneratedSlugSize'), SLUG_DICTIONARY);
|
|
||||||
if (!await FileModel.getBySlug(slug)) {
|
|
||||||
return slug;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
} while (i < tries);
|
|
||||||
throw new ServerError('Failed to generate slug; newly generated slug size should be increased by 1.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const FILE_UPLOAD_FORM_MIDDLEWARE = FILE_UPLOAD_MIDDLEWARE(() => {
|
export const FILE_UPLOAD_FORM_MIDDLEWARE = FILE_UPLOAD_MIDDLEWARE(() => {
|
||||||
const form = new IncomingForm();
|
const form = new IncomingForm();
|
||||||
form.uploadDir = 'storage/tmp';
|
form.uploadDir = 'storage/tmp';
|
||||||
form.maxFileSize = config.get<number>('max_upload_size') * 1024 * 1024;
|
form.maxFileSize = config.get<number>('max_upload_size') * 1024 * 1024;
|
||||||
|
87
src/controllers/LinkController.ts
Normal file
87
src/controllers/LinkController.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import {NextFunction, Request, Response} from "express";
|
||||||
|
import {BadRequestError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
||||||
|
import config from "config";
|
||||||
|
import {REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
|
import URLRedirect from "../models/URLRedirect";
|
||||||
|
import URLRedirectController from "./URLRedirectController";
|
||||||
|
import FileModel from "../models/FileModel";
|
||||||
|
import fs from "fs";
|
||||||
|
import Logger from "wms-core/Logger";
|
||||||
|
import generateSlug from "../SlugGenerator";
|
||||||
|
import FileController, {FILE_UPLOAD_FORM_MIDDLEWARE} from "./FileController";
|
||||||
|
|
||||||
|
export default class LinkController extends Controller {
|
||||||
|
routes(): void {
|
||||||
|
this.post('/', this.postFile, 'post-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||||
|
this.delete('/:slug', FileController.deleteFile, 'delete-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||||
|
this.get('/:slug', this.downloadFile, 'get-file');
|
||||||
|
this.put('/:slug', this.putFile, 'put-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||||
|
|
||||||
|
this.post('/', URLRedirectController.addURL, 'post-url', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||||
|
this.delete('/:slug', this.deleteURL, 'delete-url', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||||
|
this.get('/:slug', this.getURLRedirect, 'get-url');
|
||||||
|
this.put('/:slug', URLRedirectController.addURL, 'put-url', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||||
|
|
||||||
|
this.get(/(.*)/, this.domainFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async downloadFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
|
if (!file) return next();
|
||||||
|
if (file.shouldBeDeleted()) {
|
||||||
|
await fs.unlinkSync(file.storage_path);
|
||||||
|
await file.delete();
|
||||||
|
Logger.info('Deleted', file.storage_path, `(${file.real_name})`);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (file.storage_type) {
|
||||||
|
case 'local':
|
||||||
|
res.download(file.storage_path, file.real_name);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ServerError(`This file cannot be served. Download protocol for ${file.storage_type} storage type not implemented.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
if (req.body.type !== 'file') return next();
|
||||||
|
|
||||||
|
await FileController.handleFileUpload(req.body.slug || await generateSlug(10), req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async putFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
if (req.body.type !== 'file') return next();
|
||||||
|
const slug = req.params.slug;
|
||||||
|
if (!slug) throw new BadRequestError('Cannot put without a slug.', 'Either provide a slug or use POST method instead.', req.url);
|
||||||
|
|
||||||
|
await FileController.handleFileUpload(slug, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getURLRedirect(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
const url = await URLRedirect.getBySlug(req.params.slug);
|
||||||
|
if (!url) return next();
|
||||||
|
|
||||||
|
res.redirect(url.target_url, 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async deleteURL(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
const urlRedirect = await URLRedirect.getBySlug(req.params.slug);
|
||||||
|
if (!urlRedirect) return next();
|
||||||
|
|
||||||
|
throw new BadRequestError(
|
||||||
|
'Deleting url redirects is disabled for security reasons.',
|
||||||
|
'If you still want to disable the redirection, please contact us via email.',
|
||||||
|
req.url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async domainFilter(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
if (req.hostname !== config.get('domain')) {
|
||||||
|
if (req.path === '/') return res.redirect(config.get<string>('base_url'));
|
||||||
|
throw new NotFoundHttpError('Page', req.url);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
64
src/controllers/URLRedirectController.ts
Normal file
64
src/controllers/URLRedirectController.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import {NextFunction, Request, Response} from "express";
|
||||||
|
import URLRedirect from "../models/URLRedirect";
|
||||||
|
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
|
import generateSlug from "../SlugGenerator";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
|
export default class URLRedirectController extends Controller {
|
||||||
|
routes(): void {
|
||||||
|
this.get('/url/shrink', this.getURLShrinker, 'url-shrinker', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
this.get('/url/shrink/script', this.downloadLinuxScript, 'url-linux-script');
|
||||||
|
this.post('/url/shrink', this.addURLFrontend, 'shrink-url', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
this.get('/urls/:page([0-9]+)?', this.getURLRedirectManager, 'url-manager', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getURLShrinker(req: Request, res: Response): Promise<void> {
|
||||||
|
const allowedDomains = config.get<string[]>('allowed_url_domains');
|
||||||
|
res.render('url-shrinker', {
|
||||||
|
allowed_domains: allowedDomains,
|
||||||
|
default_domain: allowedDomains[config.get<number>('default_url_domain_for_urls')],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async downloadLinuxScript(req: Request, res: Response): Promise<void> {
|
||||||
|
res.download('assets/files/shrink_url.sh', 'shrink_url.sh');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getURLRedirectManager(req: Request, res: Response): Promise<void> {
|
||||||
|
res.render('url-manager', {
|
||||||
|
urls: await URLRedirect.paginateForUser(req, 100, req.models.user!.id!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async addURLFrontend(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
req.body.type = 'url';
|
||||||
|
await URLRedirectController.addURL(req, res, next, req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await generateSlug(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async addURL(req: Request, res: Response, next: NextFunction, slug?: string): Promise<void> {
|
||||||
|
if (req.body.type !== 'url') return next();
|
||||||
|
|
||||||
|
slug = slug || req.params.slug || req.body.slug || await generateSlug(10);
|
||||||
|
const urlRedirect = new URLRedirect({
|
||||||
|
user_id: req.models.user!.id,
|
||||||
|
slug: slug,
|
||||||
|
target_url: req.body.target_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
await urlRedirect.save();
|
||||||
|
|
||||||
|
const domain = req.body.url_domain || config.get<string[]>('allowed_url_domains')[config.get<number>('default_url_domain_for_urls')];
|
||||||
|
res.format({
|
||||||
|
json: () => res.json({
|
||||||
|
url: urlRedirect.getURL(domain),
|
||||||
|
}),
|
||||||
|
text: () => res.send(urlRedirect.getURL(domain)),
|
||||||
|
html: () => {
|
||||||
|
req.flash('success', 'URL shrunk successfully!');
|
||||||
|
req.flash('url', urlRedirect.getURL(domain));
|
||||||
|
res.redirectBack('/');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
src/migrations/CreateUrlRedirectsTable.ts
Normal file
20
src/migrations/CreateUrlRedirectsTable.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Migration from "wms-core/db/Migration";
|
||||||
|
import {Connection} from "mysql";
|
||||||
|
|
||||||
|
export default class CreateUrlRedirectsTable extends Migration {
|
||||||
|
public async install(connection: Connection): Promise<void> {
|
||||||
|
await this.query('CREATE TABLE url_redirects(' +
|
||||||
|
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||||
|
'user_id INT NOT NULL,' +
|
||||||
|
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
||||||
|
'target_url VARCHAR(1745) NOT NULL,' +
|
||||||
|
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||||
|
'PRIMARY KEY (id)' +
|
||||||
|
')', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
|
await this.query('DROP TABLE IF EXISTS url_redirects', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,6 +4,7 @@ import Controller from "wms-core/Controller";
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "wms-core/auth/models/User";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
|
import URLRedirect from "./URLRedirect";
|
||||||
|
|
||||||
export default class FileModel extends Model {
|
export default class FileModel extends Model {
|
||||||
public static get table(): string {
|
public static get table(): string {
|
||||||
@ -15,7 +16,7 @@ export default class FileModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise<FileModel[]> {
|
public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise<FileModel[]> {
|
||||||
return await this.paginate<FileModel>(req, perPage, this.select().where('user_id', user_id));
|
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly user_id!: number;
|
public readonly user_id!: number;
|
||||||
@ -29,7 +30,7 @@ export default class FileModel extends Model {
|
|||||||
|
|
||||||
protected init() {
|
protected init() {
|
||||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||||
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug'));
|
||||||
this.addProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
this.addProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
||||||
this.addProperty('storage_type', new Validator().defined().maxLength(64));
|
this.addProperty('storage_type', new Validator().defined().maxLength(64));
|
||||||
this.addProperty('storage_path', new Validator().defined().maxLength(1745));
|
this.addProperty('storage_path', new Validator().defined().maxLength(1745));
|
||||||
@ -38,8 +39,8 @@ export default class FileModel extends Model {
|
|||||||
this.addProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
this.addProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(): string {
|
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||||
return config.get<string>('base_url') + Controller.route('get-file', {
|
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-file', {
|
||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
44
src/models/URLRedirect.ts
Normal file
44
src/models/URLRedirect.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Model from "wms-core/db/Model";
|
||||||
|
import Validator from "wms-core/db/Validator";
|
||||||
|
import User from "wms-core/auth/models/User";
|
||||||
|
import FileModel from "./FileModel";
|
||||||
|
import {Request} from "express";
|
||||||
|
import config from "config";
|
||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
|
||||||
|
export default class URLRedirect extends Model {
|
||||||
|
public static get table(): string {
|
||||||
|
return 'url_redirects';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getBySlug(slug: string): Promise<URLRedirect | null> {
|
||||||
|
return await this.select().where('slug', slug).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise<URLRedirect[]> {
|
||||||
|
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly user_id!: number;
|
||||||
|
public readonly slug!: string;
|
||||||
|
public readonly target_url!: string;
|
||||||
|
public created_at?: Date;
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected init(): void {
|
||||||
|
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||||
|
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(URLRedirect, 'slug').unique(FileModel, 'slug'));
|
||||||
|
this.addProperty('target_url', new Validator().defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i));
|
||||||
|
this.addProperty('created_at', new Validator());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||||
|
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-url', {
|
||||||
|
slug: this.slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
159
views/desktop-utility.njk
Normal file
159
views/desktop-utility.njk
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>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
|
||||||
|
scripts/macros.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<section>
|
||||||
|
<h3>First alternative: sh script (native on linux)</h3>
|
||||||
|
<p>If you have the sh shell on your machine (i.e. you are on linux, git bash on windows...) and curl, you can
|
||||||
|
download and use these scripts:</p>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Download link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>upload_file.sh</td>
|
||||||
|
<td><a href="{{ route('file-linux-script') }}">Download</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>shrink_url.sh</td>
|
||||||
|
<td><a href="{{ route('url-linux-script') }}">Download</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>You must put a valid auth token (generated in the form at the bottom of this page) in a .ily_token
|
||||||
|
file in your home directory (<code>$HOME/.ily_token</code>).</p>
|
||||||
|
<p>Examples:</p>
|
||||||
|
<pre>upload_file.sh path/to/file</pre>
|
||||||
|
<pre>upload_file.sh path/to/file my_very_important_file.png</pre>
|
||||||
|
<pre>shrink_url.sh https://gitlab.com/ArisuOngaku/ilyli</pre>
|
||||||
|
<pre>shrink_url.sh https://gitlab.com/ArisuOngaku/ilyli repo</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Second alternative: implement your own client</h3>
|
||||||
|
<p>
|
||||||
|
To upload the file, you must:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Set the "Authorization" HTTP header to an auth token (generate one with the form below)</li>
|
||||||
|
<li>Make a proper file upload request either with the method "POST" on / (auto-generates a short
|
||||||
|
url) or "PUT" (choose the target url you want, alphanum)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Required?</th>
|
||||||
|
<th>Value(s)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th colspan="4">File upload</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>type</td>
|
||||||
|
<td>Request type</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>file</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>upload</td>
|
||||||
|
<td>File field</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>The actual file contents</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ttl</td>
|
||||||
|
<td>How much time (in seconds) to keep the file</td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>0 (never delete), 30 (delete after 30s)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>url_domain</td>
|
||||||
|
<td>Choose domain name</td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>{{ allowed_domains.join('|') }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th colspan="4">URL shrink</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>type</td>
|
||||||
|
<td>Request type</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>url</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>target_url</td>
|
||||||
|
<td>Target url</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>A valid URL starting with https:// or http://</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>url_domain</td>
|
||||||
|
<td>Choose domain name</td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>{{ allowed_domains.join('|') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>For examples with curl, please download and review the scripts above.</p>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Auth tokens</h2>
|
||||||
|
<form action="{{ route('generate-token') }}" method="POST">
|
||||||
|
{{ macros.csrf(getCSRFToken) }}
|
||||||
|
|
||||||
|
<button type="submit"><i data-feather="plus"></i> Generate a new token</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="data-table">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<form action="{{ route('revoke-token', token.id) }}" method="POST">
|
||||||
|
<button class="button danger"><i data-feather="trash"></i> Revoke</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
@ -39,7 +39,9 @@
|
|||||||
{% set expires_at = file.getExpirationDate() %}
|
{% set expires_at = file.getExpirationDate() %}
|
||||||
<td>{% if expires_at %}{{ expires_at.toISOString() }}{% else %}Never{% endif %}</td>
|
<td>{% if expires_at %}{{ expires_at.toISOString() }}{% else %}Never{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('delete-file-frontend', file.slug) }}" class="button danger"><i data-feather="trash"></i> Delete</a>
|
<form action="{{ route('delete-file-frontend', file.slug) }}" method="post">
|
||||||
|
<button class="button danger"><i data-feather="trash"></i> Delete</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
{{ macros.field(_locals, 'number', 'expire_after_days', '30', 'How many days to delete this file after', null, validation_attributes='max="1825"') }}
|
{{ macros.field(_locals, 'number', 'expire_after_days', '30', 'How many days to delete this file after', null, validation_attributes='max="1825"') }}
|
||||||
{{ macros.field(_locals, 'checkbox', 'never_expire', '', 'Never delete this file') }}
|
{{ macros.field(_locals, 'checkbox', 'never_expire', '', 'Never delete this file') }}
|
||||||
|
|
||||||
{{ macros.field(_locals, 'text', 'slug', '', 'Custom url slug', 'Example: beautiful_image.jpg sets url to https://ily.li/beautiful_image.jpg', validation_attributes='disabled') }}
|
{{ macros.field(_locals, 'text', 'slug', '', 'Custom url slug', 'Example: beautiful_image.jpg sets url to https://'+default_domain+'/beautiful_image.jpg', validation_attributes='disabled') }}
|
||||||
{{ macros.field(_locals, 'checkbox', 'autogen_url', '', 'Generate url automatically', null, validation_attributes='checked') }}
|
{{ macros.field(_locals, 'checkbox', 'autogen_url', '', 'Generate url automatically', null, validation_attributes='checked') }}
|
||||||
|
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
{{ macros.csrf(getCSRFToken) }}
|
||||||
@ -40,118 +40,17 @@
|
|||||||
<div class="content"></div>
|
<div class="content"></div>
|
||||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
{% set url = flash('url') %}
|
||||||
<h2>Setup a desktop utility</h2>
|
{% if url | length %}
|
||||||
<p>There may be a desktop client at some point. For now, if you're an advanced user, you can setup
|
<div class="copyable-text">
|
||||||
scripts/macros.</p>
|
<div class="title">URL</div>
|
||||||
|
<div class="content">{{ url }}</div>
|
||||||
<hr>
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
<section>
|
</div>
|
||||||
<h3>First alternative: sh script (native on linux)</h3>
|
{% endif %}
|
||||||
<p>If you have the sh shell on your machine (i.e. you are on linux, git bash on windows...), you can
|
|
||||||
<a href="{{ route('linux-script') }}">download this script</a>.</p>
|
|
||||||
<p>You must put a valid auth token (generated in the form at the bottom of this page) in a .ily_token file in your home directory ($HOME/.ily_token).</p>
|
|
||||||
<p>Examples:</p>
|
|
||||||
<pre>upload_script.sh path/to/file</pre>
|
|
||||||
<pre>upload_script.sh path/to/file my_very_important_file.png</pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h3>Second alternative: implement your own client</h3>
|
|
||||||
<p>
|
|
||||||
To upload the file, you must:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Set the "Authorization" HTTP header to an auth token (generate one with the form below)</li>
|
|
||||||
<li>Make a proper file upload request either with the method "POST" on / (auto-generates a short
|
|
||||||
url) or "PUT" (choose the target url you want, alphanum)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field name</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Optional?</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>upload</td>
|
|
||||||
<td>The file field</td>
|
|
||||||
<td>No</td>
|
|
||||||
<td>-</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>ttl</td>
|
|
||||||
<td>How much time (in seconds) to keep the file</td>
|
|
||||||
<td>Yes</td>
|
|
||||||
<td>0 (never delete), 30 (delete after 30s)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>Example with curl:</p>
|
|
||||||
<pre>curl -X POST -H 'Accept: application/json' \
|
|
||||||
-H "Authorization: very_secret_and_personal_token" \
|
|
||||||
-F 'upload=@path/to/local/file' \
|
|
||||||
https://ily.li/</pre>
|
|
||||||
<pre>curl -X PUT -H 'Accept: application/json' \
|
|
||||||
-H "Authorization: very_secret_and_personal_token" \
|
|
||||||
-F 'upload=@path/to/local/file' \
|
|
||||||
https://ily.li/my_very_important_file.png</pre>
|
|
||||||
<pre>curl -X POST -H 'Accept: application/json' \
|
|
||||||
-H "Authorization: very_secret_and_personal_token" \
|
|
||||||
-F 'upload=@path/to/local/file' \
|
|
||||||
-F 'ttl=30' \
|
|
||||||
https://ily.li/</pre>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="panel">
|
{% include 'desktop-utility.njk' %}
|
||||||
<h2>Auth tokens</h2>
|
|
||||||
<form action="{{ route('generate-token') }}" method="POST">
|
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
|
||||||
|
|
||||||
<button type="submit"><i data-feather="plus"></i> Generate a new token</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
<a href="{{ route('revoke-token', token.id) }}" class="button danger"><i data-feather="trash"></i> Revoke</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -8,6 +8,8 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/img/logox128.png">
|
<link rel="shortcut icon" type="image/png" href="/img/logox128.png">
|
||||||
<link rel="shortcut icon" type="image/svg" href="/img/logo.svg">
|
<link rel="shortcut icon" type="image/svg" href="/img/logo.svg">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
{% if description %}
|
{% if description %}
|
||||||
<meta name="description" content="{{ description }}">
|
<meta name="description" content="{{ description }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -14,17 +14,20 @@
|
|||||||
{% block header %}
|
{% block header %}
|
||||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {{ app.name }}</a>
|
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {{ app.name }}</a>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<button id="menu-button"><i data-feather="menu"></i></button>
|
||||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> About</a></li>
|
<ul id="main-menu">
|
||||||
|
<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-upload') }}"><i data-feather="upload"></i> File uploader</a></li>
|
<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> File manager</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-manager') }}"><i data-feather="link"></i> <span class="tip">URL manager</span></a></li>
|
||||||
|
<li><a href="{{ route('url-shrinker') }}"><i data-feather="crosshair"></i> <span class="tip">Shrink URL</span></a></li>
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> Backend</a></li>
|
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ route('logout') }}"><i data-feather="log-out"></i> Logout</a></li>
|
<li><a href="{{ route('logout') }}"><i data-feather="log-out"></i> <span class="tip">Logout</span></a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ route('auth') }}"><i data-feather="user"></i> Login / Register</a></li>
|
<li><a href="{{ route('auth') }}"><i data-feather="user"></i> <span class="tip">Login / Register</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
38
views/url-manager.njk
Normal file
38
views/url-manager.njk
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - URL manager' %}
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>URL manager</h1>
|
||||||
|
<p>These are permanent.</p>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>URL list</h2>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Target</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% 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>{{ url.target_url }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
40
views/url-shrinker.njk
Normal file
40
views/url-shrinker.njk
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - URL shrinker' %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/js/url-shrinker.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Shrink URLs</h1>
|
||||||
|
<p>(no phishing allowed)</p>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Shrink a URL</h2>
|
||||||
|
|
||||||
|
<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', 'slug', '', 'Custom url slug', 'Example: bear sets url to https://'+default_domain+'/bear', validation_attributes='disabled') }}
|
||||||
|
{{ macros.field(_locals, 'checkbox', 'autogen_url', '', 'Generate url automatically', null, validation_attributes='checked') }}
|
||||||
|
|
||||||
|
{{ macros.csrf(getCSRFToken) }}
|
||||||
|
|
||||||
|
<button type="submit"><i data-feather="link"></i> Shrink URL</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% set url = flash('url') %}
|
||||||
|
{% if url | length %}
|
||||||
|
<div class="copyable-text">
|
||||||
|
<div class="title">URL</div>
|
||||||
|
<div class="content">{{ url }}</div>
|
||||||
|
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'desktop-utility.njk' %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user