diff --git a/.eslintrc.json b/.eslintrc.json index cdf57c6..a506e1e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -88,6 +88,7 @@ }, "ignorePatterns": [ "jest.config.js", + "scripts/**/*", "webpack.config.js", "dist/**/*", "public/**/*", diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index 05403d2..e00af32 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -407,7 +407,7 @@ form { padding: 8px 16px; text-align: center; - .form-field { + .form-field:not(.hidden) { display: flex; flex-direction: column; margin: 16px auto; @@ -927,3 +927,30 @@ td.actions { .table-col-grow { width: 100%; } + +.pagination { + ul { + display: flex; + flex-direction: row; + list-style: none; + padding: 8px; + + justify-content: center; + + li { + a, &.active, &.ellipsis { + display: block; + min-width: 40px; + height: 40px; + padding: 4px; + + line-height: 32px; + text-align:center; + + &:hover:not(.active):not(.ellipsis) { + background-color: #fff5; + } + } + } + } +} diff --git a/assets/ts/fm.ts b/assets/ts/fm.ts index 07d0134..4884a1c 100644 --- a/assets/ts/fm.ts +++ b/assets/ts/fm.ts @@ -14,16 +14,38 @@ document.addEventListener('DOMContentLoaded', () => { const autogenUrlCheckbox = document.querySelector('#field-autogen_url'); const slugField = document.querySelector('#field-slug'); - if (expireAfterDaysField) { - neverExpireCheckbox?.addEventListener('change', () => { + if (expireAfterDaysField && neverExpireCheckbox) { + const updateExpireAfterDaysField = () => { expireAfterDaysField.disabled = neverExpireCheckbox.checked; - }); + + const parent = expireAfterDaysField.parentElement?.parentElement; + if (parent) { + if (neverExpireCheckbox.checked) { + parent.classList.add('hidden'); + } else { + parent.classList.remove('hidden'); + } + } + }; + neverExpireCheckbox.addEventListener('change', updateExpireAfterDaysField); + updateExpireAfterDaysField(); } - if (slugField) { - autogenUrlCheckbox?.addEventListener('change', () => { + if (slugField && autogenUrlCheckbox) { + const updateSlugField = () => { slugField.disabled = autogenUrlCheckbox.checked; - }); + + const parent = slugField.parentElement?.parentElement; + if (parent) { + if (autogenUrlCheckbox.checked) { + parent.classList.add('hidden'); + } else { + parent.classList.remove('hidden'); + } + } + }; + autogenUrlCheckbox.addEventListener('change', updateSlugField); + updateSlugField(); } let uploadForm: UploadForm | undefined; diff --git a/assets/ts/url-shrinker.ts b/assets/ts/url-shrinker.ts index 439e3d6..4835c68 100644 --- a/assets/ts/url-shrinker.ts +++ b/assets/ts/url-shrinker.ts @@ -5,9 +5,20 @@ document.addEventListener('DOMContentLoaded', () => { const autogenUrlCheckbox = document.querySelector('#field-autogen_url'); const slugField = document.querySelector('#field-slug'); - if (slugField) { - autogenUrlCheckbox?.addEventListener('change', () => { + if (slugField && autogenUrlCheckbox) { + const updateSlugField = () => { slugField.disabled = autogenUrlCheckbox.checked; - }); + + const parent = slugField.parentElement?.parentElement; + if (parent) { + if (autogenUrlCheckbox.checked) { + parent.classList.add('hidden'); + } else { + parent.classList.remove('hidden'); + } + } + }; + autogenUrlCheckbox.addEventListener('change', updateSlugField); + updateSlugField(); } }); diff --git a/config/default.json5 b/config/default.json5 index d98ccf6..e4751fb 100644 --- a/config/default.json5 +++ b/config/default.json5 @@ -54,4 +54,7 @@ ], default_url_domain_for_files: 0, default_url_domain_for_urls: 1, + + // 1 hour in ms + delete_old_files_interval: 3600000, } diff --git a/config/production.json5 b/config/production.json5 index c56ca1a..2cd1c25 100644 --- a/config/production.json5 +++ b/config/production.json5 @@ -23,8 +23,10 @@ domain: 'ily.li', allowed_url_domains: [ 'ily.li', - 'gris.li', ], default_url_domain_for_files: 0, - default_url_domain_for_urls: 1, + default_url_domain_for_urls: 0, + + // 1 day in ms + delete_old_files_interval: 86400000 } diff --git a/package.json b/package.json index 5a886b3..553d275 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ily.li", - "version": "0.6.0", + "version": "1.0.0", "description": "Self-hosted file pusher", "repository": "https://eternae.ink/arisu/ily.li", "author": "Alice Gaudon ", @@ -8,13 +8,13 @@ "main": "dist/main.js", "scripts": { "test": "jest --verbose --runInBand", - "clean": "(test ! -d dist || rm -r dist)", - "prepareSources": "cp package.json src/", + "clean": "node scripts/clean.js", + "prepare-sources": "node scripts/prepare-sources.js", "compile": "yarn clean && tsc", - "build": "yarn prepareSources && yarn compile && webpack --mode production", - "dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"", + "build": "yarn prepare-sources && yarn compile && webpack --mode production", + "dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"", "start": "yarn build && node", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx" + "lint": "eslint ." }, "devDependencies": { "@babel/core": "^7.9.0", @@ -34,7 +34,7 @@ "@typescript-eslint/eslint-plugin": "^4.3.0", "@typescript-eslint/parser": "^4.3.0", "babel-loader": "^8.1.0", - "concurrently": "^5.1.0", + "concurrently": "^6.0.0", "css-loader": "^5.0.0", "eslint": "^7.10.0", "feather-icons": "^4.28.0", @@ -43,14 +43,14 @@ "imagemin-gifsicle": "^7.0.0", "imagemin-mozjpeg": "^9.0.0", "imagemin-pngquant": "^9.0.0", - "imagemin-svgo": "^8.0.0", + "imagemin-svgo": "^9.0.0", "img-loader": "^3.0.1", "jest": "^26.1.0", "maildev": "^1.1.0", "mini-css-extract-plugin": "^1.2.1", "node-sass": "^5.0.0", "nodemon": "^2.0.3", - "sass-loader": "^10.0.1", + "sass-loader": "^11.0.1", "terser-webpack-plugin": "^5.0.3", "ts-jest": "^26.1.1", "ts-loader": "^8.0.4", diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 0000000..d5ef093 --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,10 @@ +const fs = require('fs'); + +[ + 'dist', +].forEach(file => { + if (fs.existsSync(file)) { + console.log('Cleaning', file, '...'); + fs.rmSync(file, {recursive: true}); + } +}); diff --git a/scripts/prepare-sources.js b/scripts/prepare-sources.js new file mode 100644 index 0000000..a78c2f3 --- /dev/null +++ b/scripts/prepare-sources.js @@ -0,0 +1,4 @@ +const fs = require('fs'); +const path = require('path'); + +fs.copyFileSync('package.json', path.join('src', 'package.json')); diff --git a/src/App.ts b/src/App.ts index 080315d..931fbba 100644 --- a/src/App.ts +++ b/src/App.ts @@ -42,8 +42,9 @@ import MagicLinkController from "swaf/auth/magic_link/MagicLinkController"; import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration"; import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration"; import AddPasswordToUsersMigration from "swaf/auth/password/AddPasswordToUsersMigration"; -import DropNameFromUsers from "swaf/auth/migrations/DropNameFromUsers"; +import DeleteOldFilesJobComponent from "./DeleteOldFilesJobComponent"; import packageJson = require('./package.json'); +import ReplaceTtlWithExpiresAtFilesTable from "./migrations/ReplaceTtlWithExpiresAtFilesTable"; export default class App extends Application { public constructor( @@ -69,7 +70,8 @@ export default class App extends Application { AddUsedToMagicLinksMigration, MakeMagicLinksSessionNotUniqueMigration, AddPasswordToUsersMigration, - DropNameFromUsers, + DummyMigration, + ReplaceTtlWithExpiresAtFilesTable, ]; } @@ -115,6 +117,10 @@ export default class App extends Application { // WebSocket server this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent))); + + + // Jobs + this.use(new DeleteOldFilesJobComponent()); } private registerWebSocketListeners() { diff --git a/src/DeleteOldFilesJobComponent.ts b/src/DeleteOldFilesJobComponent.ts new file mode 100644 index 0000000..f4587a3 --- /dev/null +++ b/src/DeleteOldFilesJobComponent.ts @@ -0,0 +1,62 @@ +import ApplicationComponent from "swaf/ApplicationComponent"; +import FileModel from "./models/FileModel"; +import {logger} from "swaf/Logger"; +import config from "config"; +import {WhereTest} from "swaf/db/ModelQuery"; +import MysqlComponent from "swaf/components/MysqlComponent"; +import FileController from "./controllers/FileController"; +import Timeout = NodeJS.Timeout; + +export default class DeleteOldFilesJobComponent extends ApplicationComponent { + private timeout?: Timeout; + private readonly interval: number = config.get('delete_old_files_interval'); + + public async start(): Promise { + if (this.getApp().as(MysqlComponent).canServe()) { + await this.run(); + } + + this.schedule(); + } + + public async stop(): Promise { + if (this.timeout) { + clearInterval(this.timeout); + this.timeout = undefined; + } + } + + private schedule(): void { + if (this.timeout) { + clearInterval(this.timeout); + this.timeout = undefined; + } + + this.timeout = setInterval(() => { + if (this.getApp().as(MysqlComponent).canServe()) { + this.run().catch(err => logger.error(err)); + } + }, this.interval); + logger.info(`Scheduled old file deletion job every ${this.interval}ms`); + + } + + private async run(): Promise { + const filesToDelete = await FileModel.select() + .where('expires_at', new Date(), WhereTest.LE) + .get(); + + if (filesToDelete.length > 0) { + logger.info('Deleting old files...'); + } else { + logger.info('No old file to delete.'); + return; + } + + for (const file of filesToDelete) { + logger.info(`Deleting ${file.id}`); + if (!file.shouldBeDeleted()) throw new Error('File should not be deleted.'); + await FileController.deleteFile(file); + } + } +} diff --git a/src/controllers/AuthTokenController.ts b/src/controllers/AuthTokenController.ts index 4dd1ced..a452283 100644 --- a/src/controllers/AuthTokenController.ts +++ b/src/controllers/AuthTokenController.ts @@ -3,13 +3,25 @@ import {RequireAuthMiddleware} from "swaf/auth/AuthComponent"; import {Request, Response} from "express"; import AuthToken from "../models/AuthToken"; import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "swaf/HttpError"; +import config from "config"; export default class AuthTokenController extends Controller { public routes(): void { + this.get('/auth-tokens', this.getAuthTokens, 'auth-tokens', RequireAuthMiddleware); this.post('/gen-auth-token', this.postGenAuthToken, 'generate-token', RequireAuthMiddleware); this.post('/revoke-auth-token/:id', this.postRevokeAuthToken, 'revoke-token', RequireAuthMiddleware); } + public async getAuthTokens(req: Request, res: Response): Promise { + const allowedDomains = config.get('allowed_url_domains'); + const user = req.as(RequireAuthMiddleware).getUser(); + res.render('auth-tokens', { + allowed_domains: allowedDomains, + default_domain: allowedDomains[config.get('default_url_domain_for_files')], + auth_tokens: await AuthToken.select().where('user_id', user.id).get(), + }); + } + protected async postGenAuthToken(req: Request, res: Response): Promise { const user = req.as(RequireAuthMiddleware).getUser(); const authToken = AuthToken.create({ @@ -18,7 +30,7 @@ export default class AuthTokenController extends Controller { }); await authToken.save(); req.flash('success', 'Successfully created auth token.'); - res.redirect(req.getPreviousUrl() || Controller.route('file-upload')); + res.redirect(req.getPreviousUrl() || Controller.route('file-uploader')); } protected async postRevokeAuthToken(req: Request, res: Response): Promise { @@ -34,6 +46,6 @@ export default class AuthTokenController extends Controller { await authToken.delete(); req.flash('success', 'Successfully deleted auth token.'); - res.redirect(req.getPreviousUrl() || Controller.route('file-upload')); + res.redirect(req.getPreviousUrl() || Controller.route('file-uploader')); } } diff --git a/src/controllers/FileController.ts b/src/controllers/FileController.ts index e8b4db4..a8327b1 100644 --- a/src/controllers/FileController.ts +++ b/src/controllers/FileController.ts @@ -5,8 +5,7 @@ import {BadRequestError, ForbiddenHttpError, ServerError} from "swaf/HttpError"; import FileModel from "../models/FileModel"; import config from "config"; import * as fs from "fs"; -import AuthToken from "../models/AuthToken"; -import {IncomingForm} from "formidable"; +import Formidable from "formidable"; import generateSlug from "../SlugGenerator"; import {logger} from "swaf/Logger"; import FileUploadMiddleware from "swaf/FileUploadMiddleware"; @@ -14,21 +13,20 @@ import FileUploadMiddleware from "swaf/FileUploadMiddleware"; export default class FileController extends Controller { public routes(): void { - this.get('/files/upload', this.getFileUploader, 'file-upload', RequireAuthMiddleware); - this.get('/files/upload/script', this.downloadLinuxScript, 'file-linux-script'); - this.post('/files/post', this.postFileFrontend, 'post-file-frontend', RequireAuthMiddleware, FileUploadFormMiddleware); - this.get('/files/:page([0-9]+)?', this.getFileManager, 'file-manager', RequireAuthMiddleware); + this.get('/files/:page([0-9]+)?', this.getFileUploader, 'file-uploader', RequireAuthMiddleware); + this.get('/files/upload-script', this.downloadLinuxScript, 'file-linux-script'); + this.post('/files/upload', this.postFileFrontend, 'post-file-frontend', RequireAuthMiddleware, FileUploadFormMiddleware); this.post('/files/delete/:slug', FileController.deleteFileRoute, 'delete-file-frontend', RequireAuthMiddleware); } protected async getFileUploader(req: Request, res: Response): Promise { const allowedDomains = config.get('allowed_url_domains'); const user = req.as(RequireAuthMiddleware).getUser(); - res.render('file-upload', { + res.render('file-uploader', { max_upload_size: config.get('max_upload_size'), - auth_tokens: await AuthToken.select().where('user_id', user.id).get(), allowed_domains: allowedDomains, default_domain: allowedDomains[config.get('default_url_domain_for_files')], + files: await FileModel.paginateForUser(req, 25, user.getOrFail('id')), }); } @@ -36,13 +34,6 @@ export default class FileController extends Controller { res.download('assets/files/upload_file.sh', 'upload_file.sh'); } - protected async getFileManager(req: Request, res: Response): Promise { - const user = req.as(RequireAuthMiddleware).getUser(); - res.render('file-manager', { - files: await FileModel.paginateForUser(req, 100, user.getOrFail('id')), - }); - } - protected async postFileFrontend(req: Request, res: Response): Promise { req.body.type = 'file'; await FileController.handleFileUpload( @@ -100,7 +91,7 @@ export default class FileController extends Controller { html: () => { req.flash('success', 'Upload success!'); req.flash('url', file.getURL(domain)); - res.redirect(Controller.route('file-manager')); + res.redirect(Controller.route('file-uploader')); }, }); } @@ -135,7 +126,7 @@ export default class FileController extends Controller { text: () => res.send('success'), html: () => { req.flash('success', 'Successfully deleted file.'); - res.redirect(Controller.route('file-manager')); + res.redirect(Controller.route('file-uploader')); }, }); } @@ -152,11 +143,11 @@ export class FileUploadFormMiddleware extends FileUploadMiddleware { return 'upload'; } - protected makeForm(): IncomingForm { - const form = new IncomingForm(); - form.uploadDir = 'storage/tmp'; - form.maxFileSize = config.get('max_upload_size') * 1024 * 1024; - return form; + protected makeForm(): Formidable { + return new Formidable({ + uploadDir: 'storage/tmp', + maxFileSize: config.get('max_upload_size') * 1024 * 1024, + }); } } diff --git a/src/controllers/URLRedirectController.ts b/src/controllers/URLRedirectController.ts index 631f507..5820972 100644 --- a/src/controllers/URLRedirectController.ts +++ b/src/controllers/URLRedirectController.ts @@ -4,23 +4,21 @@ import URLRedirect from "../models/URLRedirect"; import {RequireAuthMiddleware, RequireRequestAuthMiddleware} from "swaf/auth/AuthComponent"; import generateSlug from "../SlugGenerator"; import config from "config"; -import AuthToken from "../models/AuthToken"; export default class URLRedirectController extends Controller { public routes(): void { - this.get('/url/shrink', this.getUrlShrinker, 'url-shrinker', RequireAuthMiddleware); - this.get('/url/shrink/script', this.downloadLinuxScript, 'url-linux-script'); + this.get('/url/:page([0-9]+)?', this.getUrlShrinker, 'url-shrinker', RequireAuthMiddleware); + this.get('/url/shrink-script', this.downloadLinuxScript, 'url-linux-script'); this.post('/url/shrink', this.addUrlFrontend, 'shrink-url', RequireAuthMiddleware); - this.get('/urls/:page([0-9]+)?', this.getUrlRedirectManager, 'url-manager', RequireAuthMiddleware); } protected async getUrlShrinker(req: Request, res: Response): Promise { const user = req.as(RequireAuthMiddleware).getUser(); const allowedDomains = config.get('allowed_url_domains'); res.render('url-shrinker', { - auth_tokens: await AuthToken.select().where('user_id', user.id).get(), allowed_domains: allowedDomains, default_domain: allowedDomains[config.get('default_url_domain_for_urls')], + urls: await URLRedirect.paginateForUser(req, 25, user.getOrFail('id')), }); } @@ -28,13 +26,6 @@ export default class URLRedirectController extends Controller { res.download('assets/files/shrink_url.sh', 'shrink_url.sh'); } - protected async getUrlRedirectManager(req: Request, res: Response): Promise { - const user = req.as(RequireAuthMiddleware).getUser(); - res.render('url-manager', { - urls: await URLRedirect.paginateForUser(req, 100, user.getOrFail('id')), - }); - } - protected async addUrlFrontend(req: Request, res: Response, next: NextFunction): Promise { req.body.type = 'url'; await URLRedirectController.addURL( @@ -75,7 +66,7 @@ export default class URLRedirectController extends Controller { html: () => { req.flash('success', 'URL shrunk successfully!'); req.flash('url', urlRedirect.getURL(domain)); - res.redirect(Controller.route('url-manager')); + res.redirect(Controller.route('url-shrinker')); }, }); } diff --git a/src/migrations/ReplaceTtlWithExpiresAtFilesTable.ts b/src/migrations/ReplaceTtlWithExpiresAtFilesTable.ts new file mode 100644 index 0000000..fd0be5e --- /dev/null +++ b/src/migrations/ReplaceTtlWithExpiresAtFilesTable.ts @@ -0,0 +1,46 @@ +import Migration from "swaf/db/Migration"; +import FileModel from "../models/FileModel"; +import {logger} from "swaf/Logger"; + +export default class ReplaceTtlWithExpiresAtFilesTable extends Migration { + public async install(): Promise { + await this.query(`ALTER TABLE files + ADD COLUMN expires_at DATETIME NULL`); + + const files = await FileModel.select().get(); + for (const file of files) { + logger.debug(file); + if (file.created_at && typeof file.ttl === 'number' && file.ttl > 0) { + file.expires_at = new Date(file.created_at.getTime() + file.ttl * 1000); + } + const callbacks: (() => Promise)[] = []; + await file.save(this.getCurrentConnection(), callback => { + callbacks.push(callback); + }); + for (const c of callbacks) await c(); + } + + await this.query(`ALTER TABLE files + DROP COLUMN ttl`); + } + + public async rollback(): Promise { + await this.query(`ALTER TABLE files + ADD COLUMN ttl INT UNSIGNED NOT NULL DEFAULT 0`); + + const files = await FileModel.select().get(); + for (const file of files) { + if (file.created_at && file.expires_at) { + file.ttl = Math.ceil((file.expires_at.getTime() - file.created_at.getTime()) / 1000); + } + const callbacks: (() => Promise)[] = []; + await file.save(this.getCurrentConnection(), callback => { + callbacks.push(callback); + }); + for (const c of callbacks) await c(); + } + + await this.query(`ALTER TABLE files + DROP COLUMN expires_at`); + } +} diff --git a/src/models/FileModel.ts b/src/models/FileModel.ts index 328acf0..2b9ee65 100644 --- a/src/models/FileModel.ts +++ b/src/models/FileModel.ts @@ -15,6 +15,8 @@ export default class FileModel extends Model { } public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise { + req.params.sortBy = 'created_at'; + req.params.sortDirection = 'DESC'; return await this.paginate(req, perPage, this.select().where('user_id', user_id)); } @@ -26,16 +28,22 @@ export default class FileModel extends Model { public readonly storage_path?: string = undefined; public readonly size?: number = undefined; public created_at?: Date = undefined; - public readonly ttl?: number = undefined; + public expires_at?: Date | null = undefined; + + /** + * @deprecated + */ + public ttl?: number = undefined; protected init(): void { this.setValidation('user_id').defined().exists(User, 'id'); - this.setValidation('slug').defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug'); + this.setValidation('slug').defined().minLength(1).maxLength(259) + .unique(this, 'slug') + .unique(URLRedirect, 'slug'); this.setValidation('real_name').defined().minLength(1).maxLength(259); this.setValidation('storage_type').defined().maxLength(64); this.setValidation('storage_path').defined().maxLength(1745); this.setValidation('size').defined().min(0); - this.setValidation('ttl').defined().min(0).max(4294967295); } public getURL(domain: string = config.get('public_url')): string { @@ -44,15 +52,8 @@ export default class FileModel extends Model { }); } - public getExpirationDate(): Date | null { - if (!this.created_at) return new Date(); - if (this.ttl === 0) return null; - - return new Date(this.created_at.getTime() + this.getOrFail('ttl') * 1000); - } - public shouldBeDeleted(): boolean { - const expirationDate = this.getExpirationDate(); + const expirationDate = this.expires_at; if (!expirationDate) return false; return new Date().getTime() >= expirationDate.getTime(); } diff --git a/src/models/URLRedirect.ts b/src/models/URLRedirect.ts index 93d2deb..279fa5f 100644 --- a/src/models/URLRedirect.ts +++ b/src/models/URLRedirect.ts @@ -15,6 +15,8 @@ export default class URLRedirect extends Model { } public static async paginateForUser(req: Request, perPage: number, user_id: number): Promise { + req.params.sortBy = 'created_at'; + req.params.sortDirection = 'DESC'; return await this.paginate(req, perPage, this.select().where('user_id', user_id)); } @@ -26,7 +28,9 @@ export default class URLRedirect extends Model { protected init(): void { this.setValidation('user_id').defined().exists(User, 'id'); - this.setValidation('slug').defined().minLength(1).maxLength(259).unique(URLRedirect, 'slug').unique(FileModel, 'slug'); + this.setValidation('slug').defined().minLength(1).maxLength(259) + .unique(this, 'slug') + .unique(FileModel, 'slug'); this.setValidation('target_url').defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i); } diff --git a/views/desktop-utility.njk b/views/auth-tokens.njk similarity index 94% rename from views/desktop-utility.njk rename to views/auth-tokens.njk index 55760a5..eb18a2b 100644 --- a/views/desktop-utility.njk +++ b/views/auth-tokens.njk @@ -1,4 +1,53 @@ +{% extends 'layouts/base.njk' %} + +{% set title = app.name + ' - Auth tokens' %} + +{% block body %}
+
+

Auth tokens

+
+ {{ macros.csrf(getCsrfToken) }} + + +
+ + + + + + + + + + + + + + {% for token in auth_tokens %} + + + + + + + + {% endfor %} + +
#SecretCreated atLast used atActions
{{ token.id }} +
+
{{ token.secret }}
+ +
+
{{ token.created_at.toISOString() }}{{ token.used_at.toISOString() }} +
+ + {{ macros.csrf(getCsrfToken) }} +
+
+
+ +

Setup a desktop utility

There may be a desktop client at some point. For now, if you're an advanced user, you can setup @@ -32,8 +81,8 @@

Examples:

upload_file.sh path/to/file
upload_file.sh path/to/file my_very_important_file.png
-
shrink_url.sh https://gitlab.com/ArisuOngaku/ilyli
-
shrink_url.sh https://gitlab.com/ArisuOngaku/ilyli repo
+
shrink_url.sh https://eternae.ink/ashpie/ily.li
+
shrink_url.sh https://eternae.ink/ashpie/ily.li repo

@@ -114,46 +163,5 @@

For examples with curl, please download and review the scripts above.

- -
-

Auth tokens

-
- {{ macros.csrf(getCsrfToken) }} - - -
- - - - - - - - - - - - - - {% for token in auth_tokens %} - - - - - - - - {% endfor %} - -
#SecretCreated atLast used atActions
{{ token.id }} -
-
{{ token.secret }}
- -
-
{{ token.created_at.toISOString() }}{{ token.used_at.toISOString() }} -
- -
-
-
+{% endblock %} diff --git a/views/file-manager.njk b/views/file-manager.njk deleted file mode 100644 index f5fe495..0000000 --- a/views/file-manager.njk +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'layouts/base.njk' %} - -{% set title = app.name + ' - File manager' %} - -{% block scripts %} - -{% endblock %} - -{% block body %} -

File manager

-

You're their manager, please be nice with them.

- -
-
-

File list

- - - - - - - - - - - - - - {% for file in files %} - - - - - - {% set expires_at = file.getExpirationDate() %} - - - - {% endfor %} - -
#URLNameSizeExpires atActions
{{ file.id }} - -
{{ file.real_name }}
{{ (file.size / (1024 * 1024)).toFixed(2) }}MB{% if expires_at %}{{ expires_at.toISOString() }}{% else %}Never{% endif %} - {% if file.shouldBeDeleted() %} - Pending deletion - {% else %} -
- {{ macros.csrf(getCsrfToken) }} - -
- {% endif %} -
-
-
-{% endblock %} diff --git a/views/file-upload.njk b/views/file-uploader.njk similarity index 51% rename from views/file-upload.njk rename to views/file-uploader.njk index 86ecf0a..c2f4a6f 100644 --- a/views/file-upload.njk +++ b/views/file-uploader.njk @@ -1,6 +1,6 @@ {% extends 'layouts/base.njk' %} -{% set title = app.name + ' - File upload' %} +{% set title = app.name + ' - Upload file' %} {% block scripts %} @@ -8,7 +8,7 @@ {% block body %}

Upload files

-

(except illegal ones)

+

You are responsible for the files that you upload.

@@ -50,7 +50,54 @@
{% endif %} - - {% include 'desktop-utility.njk' %} + +
+

File list

+ + {{ macros.paginate(files.pagination, 'file-uploader', 3) }} + + + + + + + + + + + + + + + {% for file in files %} + + + + + + + + + {% endfor %} + +
#URLNameSizeExpires atActions
{{ file.id }} + +
{{ file.real_name }}
{{ (file.size / (1024 * 1024)).toFixed(2) }}MB{% if file.expires_at %}{{ file.expires_at.toISOString() }}{% else %}Never{% endif %} + {% if file.shouldBeDeleted() %} + Pending deletion + {% else %} +
+ {{ macros.csrf(getCsrfToken) }} + +
+ {% endif %} +
+ + {{ macros.paginate(files.pagination, 'file-uploader', 3) }} +
+ {% endblock %} diff --git a/views/layouts/base.njk b/views/layouts/base.njk index b240ed1..125872e 100644 --- a/views/layouts/base.njk +++ b/views/layouts/base.njk @@ -18,21 +18,14 @@