Add URL shortening
This commit is contained in:
parent
e34c0d46db
commit
9b5a0b2824
@ -2,6 +2,7 @@
|
||||
"bundles": {
|
||||
"app": "js/app.js",
|
||||
"fm": "js/fm.js",
|
||||
"url-shortener": "js/url-shortener.js",
|
||||
"layout": "sass/layout.scss",
|
||||
"error": "sass/error.scss",
|
||||
"logo": "img/logo.svg",
|
||||
|
17
assets/files/shorten_url.sh
Executable file
17
assets/files/shorten_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,20 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
local_file=$1
|
||||
token=$(cat $HOME/.ily_token)
|
||||
slug=$2
|
||||
token=$(cat "$HOME/.ily_token")
|
||||
|
||||
if test -z "$2"; then
|
||||
curl -X POST -H 'Accept: text/plain' \
|
||||
-H "Authorization: $token" \
|
||||
-F 'type=file' \
|
||||
-F 'upload=@'$local_file \
|
||||
-F 'ttl=2592000' \
|
||||
https://ily.li/
|
||||
if test -z "$slug"; then
|
||||
method=POST
|
||||
else
|
||||
curl -X PUT -H 'Accept: text/plain' \
|
||||
-H "Authorization: $token" \
|
||||
-F 'type=file' \
|
||||
-F 'upload=@'$local_file \
|
||||
-F 'ttl=2592000' \
|
||||
"https://ily.li/$2"
|
||||
method=PUT
|
||||
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"
|
||||
|
11
assets/js/url-shortener.js
Normal file
11
assets/js/url-shortener.js
Normal file
@ -0,0 +1,11 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('url-shortener-form');
|
||||
if (!form) return;
|
||||
|
||||
const autogenUrlCheckbox = document.getElementById('field-autogen_url');
|
||||
const slugField = document.getElementById('field-slug');
|
||||
|
||||
autogenUrlCheckbox.addEventListener('change', () => {
|
||||
slugField.disabled = autogenUrlCheckbox.checked;
|
||||
});
|
||||
});
|
@ -36,6 +36,9 @@ import CreateFilesTable from "./migrations/CreateFilesTable";
|
||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||
import BackendController from "./controllers/BackendController";
|
||||
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
||||
import AuthTokenController from "./controllers/AuthTokenController";
|
||||
import URLRedirectController from "./controllers/URLRedirectController";
|
||||
|
||||
export default class App extends Application {
|
||||
private readonly port: number;
|
||||
@ -56,6 +59,7 @@ export default class App extends Application {
|
||||
CreateFilesTable,
|
||||
IncreaseFilesSizeField,
|
||||
AddApprovedFieldToUsersTable,
|
||||
CreateUrlRedirectsTable,
|
||||
];
|
||||
}
|
||||
|
||||
@ -136,10 +140,14 @@ export default class App extends Application {
|
||||
// Core functionality
|
||||
this.use(new MailController());
|
||||
|
||||
// Other functionality
|
||||
this.use(new AuthTokenController());
|
||||
|
||||
// Semi-static
|
||||
this.use(new AboutController());
|
||||
|
||||
// Global slug
|
||||
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,27 +1,24 @@
|
||||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
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 {cryptoRandomDictionary} from "wms-core/Utils";
|
||||
import config from "config";
|
||||
import * as fs from "fs";
|
||||
import AuthToken from "../models/AuthToken";
|
||||
import {IncomingForm} from "formidable";
|
||||
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 {
|
||||
routes(): void {
|
||||
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.get('/files/:page([0-9]+)?', this.getFileManager, 'file-manager', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/files/delete/:slug', this.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
|
||||
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);
|
||||
|
||||
this.post('/', this.postFile, 'post-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||
this.delete('/:slug', this.deleteFile, 'delete-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||
@ -46,32 +43,9 @@ 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 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'));
|
||||
}
|
||||
|
||||
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 this.handleFileUpload(req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await generateSlug(10), req, res);
|
||||
}
|
||||
|
||||
protected async downloadFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
@ -96,7 +70,7 @@ export default class FileController extends Controller {
|
||||
protected async postFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
if (req.body.type !== 'file') return next();
|
||||
|
||||
await this.handleFileUpload(req.body.slug || await this.generateSlug(10), req, res);
|
||||
await this.handleFileUpload(req.body.slug || await generateSlug(10), req, res);
|
||||
}
|
||||
|
||||
protected async putFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
@ -174,18 +148,6 @@ 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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
82
src/controllers/URLRedirectController.ts
Normal file
82
src/controllers/URLRedirectController.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import Controller from "wms-core/Controller";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import URLRedirect from "../models/URLRedirect";
|
||||
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import generateSlug from "../SlugGenerator";
|
||||
import {BadRequestError} from "wms-core/HttpError";
|
||||
|
||||
export default class URLRedirectController extends Controller {
|
||||
routes(): void {
|
||||
this.get('/url/shorten', this.getURLShortener, 'url-shortener', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.get('/url/shorten/script', this.downloadLinuxScript, 'url-linux-script');
|
||||
this.post('/url/shorten', this.addURLFrontend, 'shorten-url', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.get('/urls/:page([0-9]+)?', this.getURLRedirectManager, 'url-manager', REQUIRE_AUTH_MIDDLEWARE);
|
||||
|
||||
this.post('/', this.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', this.addURL, 'put-url', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||
}
|
||||
|
||||
protected async getURLShortener(req: Request, res: Response): Promise<void> {
|
||||
res.render('url-shortener');
|
||||
}
|
||||
|
||||
protected async downloadLinuxScript(req: Request, res: Response): Promise<void> {
|
||||
res.download('assets/files/shorten_url.sh', 'shorten_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 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 addURLFrontend(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
req.body.type = 'url';
|
||||
await this.addURL(req, res, next, req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await generateSlug(10));
|
||||
}
|
||||
|
||||
protected 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();
|
||||
|
||||
res.format({
|
||||
json: () => res.json({
|
||||
url: urlRedirect.getURL(),
|
||||
}),
|
||||
text: () => res.send(urlRedirect.getURL()),
|
||||
html: () => {
|
||||
req.flash('success', 'URL shortened successfully!');
|
||||
req.flash('url', urlRedirect.getURL());
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
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(): string {
|
||||
return config.get<string>('base_url') + Controller.route('get-url', {
|
||||
slug: this.slug,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
119
views/desktop-utility.njk
Normal file
119
views/desktop-utility.njk
Normal file
@ -0,0 +1,119 @@
|
||||
<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...), you can
|
||||
<a href="{{ route('file-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 'type=file' \
|
||||
-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 'type=file' \
|
||||
-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 'type=file' \
|
||||
-F 'upload=@path/to/local/file' \
|
||||
-F 'ttl=30' \
|
||||
https://ily.li/</pre>
|
||||
</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>
|
@ -41,122 +41,7 @@
|
||||
<button class="copy-button"><i data-feather="copy"></i></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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...), 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 'type=file' \
|
||||
-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 'type=file' \
|
||||
-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 'type=file' \
|
||||
-F 'upload=@path/to/local/file' \
|
||||
-F 'ttl=30' \
|
||||
https://ily.li/</pre>
|
||||
</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>
|
||||
{% include 'desktop-utility.njk' %}
|
||||
{% endblock %}
|
@ -15,10 +15,11 @@
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {{ app.name }}</a>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> About</a></li>
|
||||
{% 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> File manager</a></li>
|
||||
<li><a href="{{ route('url-shortener') }}"><i data-feather="crosshair"></i> URL shortener</a></li>
|
||||
<li><a href="{{ route('url-manager') }}"><i data-feather="link"></i> URL manager</a></li>
|
||||
{% if user.is_admin %}
|
||||
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> Backend</a></li>
|
||||
{% endif %}
|
||||
@ -47,4 +48,4 @@
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}{{ app.name }} v{{ app_version }} - all rights reserved.{% endblock %}
|
||||
{% block footer %}{{ app.name }} v{{ app_version }} - all rights reserved. | <a href="{{ route('about') }}">About</a>{% endblock %}
|
44
views/url-manager.njk
Normal file
44
views/url-manager.njk
Normal file
@ -0,0 +1,44 @@
|
||||
{% 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>
|
||||
<th>Actions</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>
|
||||
<td>
|
||||
<form action="{{ route('delete-file-frontend', url.slug) }}" method="post">
|
||||
<button class="button danger"><i data-feather="trash"></i> Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
40
views/url-shortener.njk
Normal file
40
views/url-shortener.njk
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - URL shortener' %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/url-shortener.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Shorten URLs</h1>
|
||||
<p>(no phishing allowed)</p>
|
||||
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h2>Shorten a URL</h2>
|
||||
|
||||
<form action="{{ route('shorten-url') }}" method="POST" id="url-shortener-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://ily.li/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> Shorten 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 %}
|
52
yarn.lock
52
yarn.lock
@ -770,13 +770,20 @@
|
||||
"@babel/types" "^7.4.4"
|
||||
esutils "^2.0.2"
|
||||
|
||||
"@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.8.4":
|
||||
version "7.10.3"
|
||||
resolved "https://registry.toot.party/@babel%2fruntime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
|
||||
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.8.7":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.toot.party/@babel%2fruntime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
|
||||
integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.1", "@babel/template@^7.10.3", "@babel/template@^7.3.3":
|
||||
version "7.10.3"
|
||||
resolved "https://registry.toot.party/@babel%2ftemplate/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8"
|
||||
@ -1270,9 +1277,9 @@
|
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||
|
||||
"@types/redis@^2.8.18":
|
||||
version "2.8.22"
|
||||
resolved "https://registry.toot.party/@types%2fredis/-/redis-2.8.22.tgz#8935227cbe39080506b625276d64974ddbcb9ea4"
|
||||
integrity sha512-O21YLcAtcSzax8wy4CfxMNjIMNf5X2c1pKTXDWLMa2p77Igvy7wuNjWVv+Db93wTvRvLLev6oq3IE7gxNKFZyg==
|
||||
version "2.8.24"
|
||||
resolved "https://registry.toot.party/@types%2fredis/-/redis-2.8.24.tgz#164d1d1b9f628d089e63874b072100c30d9d1c1a"
|
||||
integrity sha512-I5LivNsNdEvBDJnBthjL4YAqD888eSf/k8kM5ROzgRHy7UE7n+ijwFVKqCSM4Ekujfb+3SDz/GbEhXWjnuI04w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@ -1290,9 +1297,9 @@
|
||||
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
||||
|
||||
"@types/ws@^7.2.4":
|
||||
version "7.2.5"
|
||||
resolved "https://registry.toot.party/@types%2fws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49"
|
||||
integrity sha512-4UEih9BI1nBKii385G9id1oFrSkLcClbwtDfcYj8HJLQqZVAtb/42vXVrYvRWCcufNF/a+rZD3MxNwghA7UmCg==
|
||||
version "7.2.6"
|
||||
resolved "https://registry.toot.party/@types%2fws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d"
|
||||
integrity sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@ -1519,7 +1526,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
|
||||
resolved "https://registry.toot.party/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773"
|
||||
integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==
|
||||
|
||||
ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5:
|
||||
ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2:
|
||||
version "6.12.2"
|
||||
resolved "https://registry.toot.party/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
||||
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
|
||||
@ -1529,6 +1536,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.12.3"
|
||||
resolved "https://registry.toot.party/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
|
||||
integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
amdefine@>=0.0.4:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.toot.party/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||
@ -6785,9 +6802,9 @@ no-case@^2.2.0:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-addon-api@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.toot.party/node-addon-api/-/node-addon-api-2.0.1.tgz#4fd0931bf6d7e48b219ff3e6abc73cbb0252b7a3"
|
||||
integrity sha512-2WVfwRfIr1AVn3dRq4yRc2Hn35ND+mPJH6inC6bjpYCZVrpXPB4j3T6i//OGVfqVsR1t/X/axRulDsheq4F0LQ==
|
||||
version "2.0.2"
|
||||
resolved "https://registry.toot.party/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
|
||||
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
|
||||
|
||||
node-gyp@^3.8.0:
|
||||
version "3.8.0"
|
||||
@ -9825,9 +9842,9 @@ widest-line@^3.1.0:
|
||||
string-width "^4.0.0"
|
||||
|
||||
wms-core@^0:
|
||||
version "0.10.18"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.10.18.tgz#450bf5be19f1a5872230199b04353169e3cfe2aa"
|
||||
integrity sha512-x4+FgnfovJHLpAKYKmh5+rM9Zovklya8mZC+KnrQ65y2v3vw3i9hDRjXeums5OvyQsBA7X4OObEffTbClLNCTQ==
|
||||
version "0.10.19"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.10.19.tgz#d04830bd66ea5ee2e43b7314dfa8c92e6b11697f"
|
||||
integrity sha512-9DGTKtdKho8FJV5b0k7D6/21zFjOQTbJz4wkvTdgW+EL2iriH9ojFVRaakGqgb3WoxejZFaz14HSYysNAJ4Xsw==
|
||||
dependencies:
|
||||
"@types/express" "^4.17.6"
|
||||
"@types/express-session" "^1.17.0"
|
||||
@ -9901,11 +9918,16 @@ write-file-atomic@^3.0.0:
|
||||
signal-exit "^3.0.2"
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
|
||||
ws@^7.0.0, ws@^7.2.3:
|
||||
ws@^7.0.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.toot.party/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
||||
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
|
||||
|
||||
ws@^7.2.3:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.toot.party/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
|
||||
|
||||
xdg-basedir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.toot.party/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||
|
Loading…
Reference in New Issue
Block a user