Actually make file uploads work

This commit is contained in:
Alice Gaudon 2020-06-14 21:23:57 +02:00
parent 6db8b1cd21
commit a968cf8408
11 changed files with 273 additions and 135 deletions

View File

@ -1,9 +1,17 @@
document.addEventListener('DOMContentLoaded', () => {
if (!document.getElementById('upload-form')) return;
const neverExpireCheckbox = document.getElementById('field-never_expire');
const expireAfterDaysField = document.getElementById('field-expire_after_days');
const autogenUrlCheckbox = document.getElementById('field-autogen_url');
const slugField = document.getElementById('field-slug');
neverExpireCheckbox.addEventListener('change', () => {
// noinspection RedundantConditionalExpressionJS
expireAfterDaysField.disabled = neverExpireCheckbox.checked ? true : false;
expireAfterDaysField.disabled = neverExpireCheckbox.checked;
});
autogenUrlCheckbox.addEventListener('change', () => {
slugField.disabled = autogenUrlCheckbox.checked;
});
});

View File

@ -18,8 +18,8 @@
"@babel/preset-env": "^7.9.5",
"@types/config": "^0.0.36",
"@types/express": "^4.17.6",
"@types/formidable": "^1.0.31",
"@types/jest": "^25.2.1",
"@types/multer": "^1.4.3",
"@types/node": "^13.13.2",
"babel-loader": "^8.1.0",
"concurrently": "^5.1.0",
@ -47,6 +47,6 @@
"dependencies": {
"config": "^3.3.1",
"express": "^4.17.1",
"multer": "^1.4.2"
"formidable": "^1.2.2"
}
}

View File

@ -64,23 +64,15 @@ export default class App extends Application {
const redisComponent = new RedisComponent();
const mysqlComponent = new MysqlComponent();
// Session
this.use(redisComponent);
this.use(new SessionComponent(redisComponent));
// Static files
this.use(new ServeStaticDirectoryComponent('public'));
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
// Utils
this.use(new RedirectBackComponent());
this.use(new FormHelperComponent());
const expressAppComponent = new ExpressAppComponent(this.port);
this.use(expressAppComponent);
this.use(new NunjucksComponent());
this.use(new LogRequestsComponent());
// Static files
this.use(new ServeStaticDirectoryComponent('public'));
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
// Maintenance
this.use(new MaintenanceComponent(this, () => {
return redisComponent.canServe() && mysqlComponent.canServe();
@ -91,7 +83,9 @@ export default class App extends Application {
this.use(mysqlComponent);
this.use(new MailComponent());
// Auth
// Session
this.use(redisComponent);
this.use(new SessionComponent(redisComponent));
this.use(new AuthComponent(new class extends AuthGuard<MagicLink | AuthToken> {
public async getProofForSession(session: Express.Session): Promise<any | null> {
return await MagicLink.bySessionID(session.id, [MagicLinkActionType.LOGIN, MagicLinkActionType.REGISTER]);
@ -100,13 +94,22 @@ export default class App extends Application {
public async getProofForRequest(req: Request): Promise<MagicLink | AuthToken | null> {
const authorization = req.header('Authorization');
if (authorization) {
return await AuthToken.getBySecret(authorization);
const token = await AuthToken.getBySecret(authorization);
if (token) {
token.use();
await token.save();
}
return token;
}
return super.getProofForRequest(req);
}
}));
// Utils
this.use(new RedirectBackComponent());
this.use(new FormHelperComponent());
// Middlewares
this.use(new CsrfProtectionComponent());

View File

@ -1,39 +1,74 @@
import Controller from "wms-core/Controller";
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
import {Request, Response, Router} from "express";
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
import {Request, Response} from "express";
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError, 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 multer from "multer";
import AuthToken from "../models/AuthToken";
import {IncomingForm} from "formidable";
import {FILE_UPLOAD_MIDDLEWARE} from "wms-core/components/ExpressAppComponent";
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
export default class FileController extends Controller {
setupRequestParsingMiddlewares(router: Router) {
router.use('/', FILE_UPLOAD_MIDDLEWARE);
router.use('/:slug', FILE_UPLOAD_MIDDLEWARE);
routes(): void {
this.get('/files/upload', this.getFileUploader, 'file-upload', REQUIRE_AUTH_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/delete/:slug', this.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);
}
routes(): void {
this.get('/files/:page?', this.getFileManager, 'file-manager', REQUIRE_AUTH_MIDDLEWARE);
this.get('/files/delete/:slug', this.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
this.post('/', this.postFile, 'post-file', REQUIRE_AUTH_MIDDLEWARE);
this.delete('/delete/:slug', this.deleteFile, 'delete-file', REQUIRE_AUTH_MIDDLEWARE);
this.get('/:slug', this.downloadFile, 'get-file');
this.put('/:slug', this.putFile, 'put-file', REQUIRE_AUTH_MIDDLEWARE);
protected async getFileUploader(req: Request, res: Response): Promise<void> {
res.render('file-upload', {
max_upload_size: config.get<string>('max_upload_size'),
auth_tokens: await AuthToken.getForUser(req.models.user!.id!),
});
}
protected async getFileManager(req: Request, res: Response): Promise<void> {
res.render('file-manager', {
files: await FileModel.paginate<FileModel>(req, 100),
max_upload_size: config.get<string>('max_upload_size'),
files: await FileModel.paginateForUser(req, 100, req.models.user!.id!),
});
}
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>(`${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);
}
protected async downloadFile(req: Request, res: Response): Promise<void> {
console.log('heeey');
const file = await FileModel.getBySlug(req.params.slug);
if (!file || file.shouldBeDeleted()) throw new NotFoundHttpError('File', req.url);
@ -59,11 +94,11 @@ export default class FileController extends Controller {
protected async handleFileUpload(slug: string, req: Request, res: Response): Promise<void> {
// Check for file upload
if (!req.file) {
if (!req.files || !req.files['upload']) {
throw new BadRequestError('No file received.', 'You must upload exactly one (1) file.', req.url);
}
let upload = req.file;
let upload = req.files['upload'];
// TTL
let ttl = config.get<number>('default_file_ttl');
@ -74,7 +109,7 @@ export default class FileController extends Controller {
const file = new FileModel({
user_id: req.models.user!.id,
slug: slug,
real_name: upload.originalname,
real_name: upload.name,
storage_type: 'local',
storage_path: 'storage/uploads/' + slug,
size: upload.size,
@ -138,9 +173,10 @@ export default class FileController extends Controller {
}
}
const FILE_UPLOAD_MIDDLEWARE = multer({
dest: 'storage/tmp',
limits: {
fileSize: config.get<number>('max_upload_size') * 1024 * 1024,
},
}).single('upload');
const FILE_UPLOAD_FORM_MIDDLEWARE = FILE_UPLOAD_MIDDLEWARE(() => {
const form = new IncomingForm();
form.uploadDir = 'storage/tmp';
form.maxFileSize = config.get<number>('max_upload_size') * 1024 * 1024;
return form;
}, 'upload');

View File

@ -8,6 +8,7 @@ export default class CreateAuthTokensTable extends Migration {
'user_id INT NOT NULL,' +
'secret VARCHAR(64) UNIQUE NOT NULL,' +
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
'used_at DATETIME NOT NULL DEFAULT NOW(),' +
'ttl INT UNSIGNED NOT NULL,' +
'PRIMARY KEY (id)' +
')', connection);

View File

@ -3,6 +3,7 @@ import AuthProof from "wms-core/auth/AuthProof";
import UserEmail from "wms-core/auth/models/UserEmail";
import User from "wms-core/auth/models/User";
import Validator from "wms-core/db/Validator";
import {cryptoRandomDictionary} from "wms-core/Utils";
export default class AuthToken extends Model implements AuthProof {
public static async getBySecret(secret: string): Promise<AuthToken | null> {
@ -10,19 +11,41 @@ export default class AuthToken extends Model implements AuthProof {
return models.length > 0 ? models[0] : null;
}
public static async getForUser(user_id: number): Promise<AuthToken[]> {
return await this.models<AuthToken>(this.select().where('user_id', user_id));
}
protected readonly user_id!: number;
protected readonly secret!: string;
protected created_at?: Date;
protected used_at?: Date;
protected readonly ttl!: number;
constructor(props: any) {
super(props);
if (!this.secret) {
this.secret = cryptoRandomDictionary(64, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_');
}
}
protected defineProperties() {
this.defineProperty('user_id', new Validator().defined().exists(User, 'id'));
this.defineProperty('secret', new Validator().defined().between(32, 64));
this.defineProperty('created_at', new Validator());
this.defineProperty('used_at', new Validator());
this.defineProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
}
public use() {
this.used_at = new Date();
}
public canDelete(user_id: number) {
return this.user_id === user_id;
}
public getExpirationDate(): Date {
if (!this.created_at) return new Date();

View File

@ -3,6 +3,7 @@ import Validator from "wms-core/db/Validator";
import Controller from "wms-core/Controller";
import config from "config";
import User from "wms-core/auth/models/User";
import {Request} from "express";
export default class FileModel extends Model {
public static get table(): string {
@ -14,6 +15,10 @@ export default class FileModel extends Model {
return models.length > 0 ? models[0] : null;
}
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));
}
public readonly user_id!: number;
public readonly slug!: string;
public readonly real_name!: string;

View File

@ -10,24 +10,6 @@
<h1>File manager</h1>
<p>You're their manager, please be nice with them.</p>
<div class="container">
<section class="panel">
<h2>Upload a file</h2>
<form action="{{ route('post-file') }}" method="POST" enctype="multipart/form-data">
{{ macros.field(_locals, 'file', 'upload', '', 'Choose wisely', 'The maximum upload size is ' + max_upload_size + 'MiB', validation_attributes='required') }}
{{ 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.csrf(getCSRFToken) }}
<button type="submit"><i data-feather="upload"></i> Upload</button>
</form>
</section>
</div>
<section class="panel">
<h2>File list</h2>
<table class="data-table">

128
views/file-upload.njk Normal file
View File

@ -0,0 +1,128 @@
{% extends 'layouts/base.njk' %}
{% set title = 'ily.li - File manager' %}
{% block scripts %}
<script src="/js/fm.js"></script>
{% endblock %}
{% block body %}
<h1>Upload files</h1>
<p>(except illegal ones)</p>
<div class="container">
<section class="panel">
<h2>Upload a file</h2>
<form action="{{ route('post-file-frontend') }}" method="POST" enctype="multipart/form-data" id="upload-form">
{{ macros.field(_locals, 'file', 'upload', '', 'Choose wisely', 'The maximum upload size is ' + max_upload_size + 'MiB', validation_attributes='required') }}
{{ 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, 'text', 'slug', '', 'Custom url slug', 'Example: beautiful_image.jpg sets url to https://ily.li/beautiful_image.jpg', 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="upload"></i> Upload</button>
</form>
</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>
<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>
</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>
<a href="{{ route('revoke-token', token.id) }}" class="button danger"><i
data-feather="trash"></i> Revoke</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}

View File

@ -17,6 +17,7 @@
<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('logout') }}"><i data-feather="log-out"></i> Logout</a></li>
{% else %}

View File

@ -1110,6 +1110,11 @@
dependencies:
"@types/node" "*"
"@types/events@*":
version "3.0.0"
resolved "https://registry.toot.party/@types%2fevents/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/express-serve-static-core@*":
version "4.17.7"
resolved "https://registry.toot.party/@types%2fexpress-serve-static-core/-/express-serve-static-core-4.17.7.tgz#dfe61f870eb549dc6d7e12050901847c7d7e915b"
@ -1137,6 +1142,14 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/formidable@^1.0.31":
version "1.0.31"
resolved "https://registry.toot.party/@types%2fformidable/-/formidable-1.0.31.tgz#274f9dc2d0a1a9ce1feef48c24ca0859e7ec947b"
integrity sha512-dIhM5t8lRP0oWe2HF8MuPvdd1TpPTjhDMAqemcq6oIZQCBQTovhBAdTQ5L5veJB4pdQChadmHuxtB0YzqvfU3Q==
dependencies:
"@types/events" "*"
"@types/node" "*"
"@types/glob@^7.1.1":
version "7.1.2"
resolved "https://registry.toot.party/@types%2fglob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987"
@ -1195,13 +1208,6 @@
resolved "https://registry.toot.party/@types%2fminimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/multer@^1.4.3":
version "1.4.3"
resolved "https://registry.toot.party/@types%2fmulter/-/multer-1.4.3.tgz#bdff74b334c38a8ee1de9fbedb5d1d3dbc377422"
integrity sha512-tWsKbF5LYtXrJ7eOfI0aLBgEv9B7fnJe1JRXTj5+Z6EMfX0yHVsRFsNGnKyN8Bs0gtDv+JR37xAqsPnALyVTqg==
dependencies:
"@types/express" "*"
"@types/mysql@^2.15.10":
version "2.15.13"
resolved "https://registry.toot.party/@types%2fmysql/-/mysql-2.15.13.tgz#153dc2e2f8dffd39f7bba556c2679f14bdbecde1"
@ -1591,11 +1597,6 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
append-field@^1.0.0:
version "1.0.0"
resolved "https://registry.toot.party/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.toot.party/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -2210,14 +2211,6 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.toot.party/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
busboy@^0.2.11:
version "0.2.14"
resolved "https://registry.toot.party/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
dependencies:
dicer "0.2.5"
readable-stream "1.1.x"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.toot.party/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@ -2623,7 +2616,7 @@ concat-map@0.0.1:
resolved "https://registry.toot.party/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concat-stream@^1.5.0, concat-stream@^1.5.2:
concat-stream@^1.5.0:
version "1.6.2"
resolved "https://registry.toot.party/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@ -3210,14 +3203,6 @@ detect-newline@^3.0.0:
resolved "https://registry.toot.party/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
dicer@0.2.5:
version "0.2.5"
resolved "https://registry.toot.party/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
dependencies:
readable-stream "1.1.x"
streamsearch "0.1.2"
diff-sequences@^25.2.6:
version "25.2.6"
resolved "https://registry.toot.party/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
@ -4066,6 +4051,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formidable@^1.2.2:
version "1.2.2"
resolved "https://registry.toot.party/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9"
integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.toot.party/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5190,11 +5180,6 @@ is-yarn-global@^0.3.0:
resolved "https://registry.toot.party/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.toot.party/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.toot.party/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -6784,20 +6769,6 @@ ms@^2.1.1:
resolved "https://registry.toot.party/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
multer@^1.4.2:
version "1.4.2"
resolved "https://registry.toot.party/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a"
integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==
dependencies:
append-field "^1.0.0"
busboy "^0.2.11"
concat-stream "^1.5.2"
mkdirp "^0.5.1"
object-assign "^4.1.1"
on-finished "^2.3.0"
type-is "^1.6.4"
xtend "^4.0.0"
mysql@^2.18.1:
version "2.18.1"
resolved "https://registry.toot.party/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
@ -8049,16 +8020,6 @@ read-pkg@^5.2.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@1.1.x:
version "1.1.14"
resolved "https://registry.toot.party/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^3.1.1, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.toot.party/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
@ -8931,11 +8892,6 @@ stream-shift@^1.0.0:
resolved "https://registry.toot.party/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
streamsearch@0.1.2:
version "0.1.2"
resolved "https://registry.toot.party/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.toot.party/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@ -9007,11 +8963,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.toot.party/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.toot.party/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@ -9480,7 +9431,7 @@ type-fest@^0.8.1:
resolved "https://registry.toot.party/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.toot.party/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@ -9974,13 +9925,12 @@ widest-line@^3.1.0:
string-width "^4.0.0"
wms-core@^0:
version "0.7.12"
resolved "https://registry.toot.party/wms-core/-/wms-core-0.7.12.tgz#9e198599df089962f636de7b28fa10d96f4167b7"
integrity sha512-msKHjh+7QnPugpcxrLcSiVFBemtGkSQOG0fLFIRoTk5CORbZHLdghBeL7DGmSsfM1SV97om2XI9xSjz2A/Rj1Q==
version "0.8.10"
resolved "https://registry.toot.party/wms-core/-/wms-core-0.8.10.tgz#8db64926f9bd9eaa61556ad98dcd215361b16950"
integrity sha512-1GJkpOX/efoDGK9JOwHnBkD55Q6dEKTQsdHpgMmF3g0h7XxJFAAsZTv0tOyfYHIEpJo5mNdR/+mzdZTPsgtAdw==
dependencies:
"@types/express" "^4.17.6"
"@types/express-session" "^1.17.0"
"@types/multer" "^1.4.3"
"@types/mysql" "^2.15.10"
"@types/nodemailer" "^6.4.0"
"@types/nunjucks" "^3.1.3"
@ -9994,6 +9944,7 @@ wms-core@^0:
cookie-parser "^1.4.5"
express "^4.17.1"
express-session "^1.17.1"
formidable "^1.2.2"
geoip-lite "^1.4.2"
mjml "^4.6.2"
mysql "^2.18.1"