Merge branch 'develop'
This commit is contained in:
commit
b0053b3523
18
assets/files/upload_file.sh
Executable file
18
assets/files/upload_file.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
local_file=$1
|
||||||
|
token=$(cat $HOME/.ily_token)
|
||||||
|
|
||||||
|
if test -z "$2"; then
|
||||||
|
curl -X POST -H 'Accept: text/plain' \
|
||||||
|
-H "Authorization: $token" \
|
||||||
|
-F 'upload=@'$local_file \
|
||||||
|
-F 'ttl=2592000' \
|
||||||
|
https://ily.li/
|
||||||
|
else
|
||||||
|
curl -X PUT -H 'Accept: text/plain' \
|
||||||
|
-H "Authorization: $token" \
|
||||||
|
-F 'upload=@'$local_file \
|
||||||
|
-F 'ttl=2592000' \
|
||||||
|
"https://ily.li/$2"
|
||||||
|
fi
|
@ -407,6 +407,14 @@ button, .button {
|
|||||||
thead tr:hover {
|
thead tr:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th.shrink-col {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-content {
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
export default Object.assign(require("wms-core/config/default").default, {
|
export default Object.assign(require("wms-core/config/default").default, {
|
||||||
|
app: {
|
||||||
|
name: 'ily.li',
|
||||||
|
contact_email: 'contact@ily.li'
|
||||||
|
},
|
||||||
domain: 'localhost',
|
domain: 'localhost',
|
||||||
base_url: 'http://localhost:4893',
|
base_url: 'http://localhost:4893',
|
||||||
public_websocket_url: 'ws://localhost:4893',
|
public_websocket_url: 'ws://localhost:4893',
|
||||||
@ -18,4 +22,5 @@ export default Object.assign(require("wms-core/config/default").default, {
|
|||||||
newlyGeneratedSlugSize: 3,
|
newlyGeneratedSlugSize: 3,
|
||||||
default_file_ttl: 30, // 30 seconds
|
default_file_ttl: 30, // 30 seconds
|
||||||
max_upload_size: 1, // MB
|
max_upload_size: 1, // MB
|
||||||
|
approval_mode: false,
|
||||||
});
|
});
|
@ -16,4 +16,5 @@ export default Object.assign(require("wms-core/config/production").default, {
|
|||||||
newlyGeneratedSlugSize: 5,
|
newlyGeneratedSlugSize: 5,
|
||||||
default_file_ttl: 30 * 24 * 3600, // 30 days
|
default_file_ttl: 30 * 24 * 3600, // 30 days
|
||||||
max_upload_size: 8192, // MB
|
max_upload_size: 8192, // MB
|
||||||
|
approval_mode: true,
|
||||||
});
|
});
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ily.li",
|
"name": "ily.li",
|
||||||
"version": "0.2.2",
|
"version": "0.3.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>",
|
||||||
|
@ -34,6 +34,8 @@ import {MagicLinkActionType} from "./controllers/MagicLinkActionType";
|
|||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
import CreateFilesTable from "./migrations/CreateFilesTable";
|
import CreateFilesTable from "./migrations/CreateFilesTable";
|
||||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||||
|
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||||
|
import BackendController from "./controllers/BackendController";
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
@ -53,6 +55,7 @@ export default class App extends Application {
|
|||||||
CreateAuthTokensTable,
|
CreateAuthTokensTable,
|
||||||
CreateFilesTable,
|
CreateFilesTable,
|
||||||
IncreaseFilesSizeField,
|
IncreaseFilesSizeField,
|
||||||
|
AddApprovedFieldToUsersTable,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ export default class App extends Application {
|
|||||||
public async getProofForRequest(req: Request): Promise<MagicLink | AuthToken | null> {
|
public async getProofForRequest(req: Request): Promise<MagicLink | AuthToken | null> {
|
||||||
const authorization = req.header('Authorization');
|
const authorization = req.header('Authorization');
|
||||||
if (authorization) {
|
if (authorization) {
|
||||||
const token = await AuthToken.getBySecret(authorization);
|
const token = await AuthToken.select().where('secret', authorization).first();
|
||||||
if (token) {
|
if (token) {
|
||||||
token.use();
|
token.use();
|
||||||
await token.save();
|
await token.save();
|
||||||
@ -128,6 +131,7 @@ export default class App extends Application {
|
|||||||
// Priority
|
// Priority
|
||||||
this.use(new AuthController());
|
this.use(new AuthController());
|
||||||
this.use(new MagicLinkController(this.magicLinkWebSocketListener!));
|
this.use(new MagicLinkController(this.magicLinkWebSocketListener!));
|
||||||
|
this.use(new BackendController());
|
||||||
|
|
||||||
// Core functionality
|
// Core functionality
|
||||||
this.use(new MailController());
|
this.use(new MailController());
|
||||||
|
12
src/Mails.ts
Normal file
12
src/Mails.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {MailTemplate} from "wms-core/Mail";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
|
export const ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||||
|
'account_review_notice',
|
||||||
|
data => `Your account was ${data.approved ? 'approved' : 'rejected'}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||||
|
'pending_account_review',
|
||||||
|
() => 'A new account is pending review on ' + config.get<string>('domain'),
|
||||||
|
);
|
69
src/controllers/BackendController.ts
Normal file
69
src/controllers/BackendController.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import User from "wms-core/auth/models/User";
|
||||||
|
import {NotFoundHttpError} from "wms-core/HttpError";
|
||||||
|
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
|
||||||
|
import Mail from "wms-core/Mail";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
|
export default class BackendController extends Controller {
|
||||||
|
getRoutesPrefix(): string {
|
||||||
|
return '/backend';
|
||||||
|
}
|
||||||
|
|
||||||
|
routes(): void {
|
||||||
|
this.get('/', this.getIndex, 'backend', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||||
|
if (User.isApprovalMode()) {
|
||||||
|
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||||
|
this.get('/accounts-approval/approve/:id', this.getApproveAccount, 'approve-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||||
|
this.get('/accounts-approval/reject/:id', this.getRejectAccount, 'reject-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIndex(req: Request, res: Response): Promise<void> {
|
||||||
|
res.render('backend/index', {
|
||||||
|
approval_mode: User.isApprovalMode(),
|
||||||
|
accounts_to_approve: User.isApprovalMode() ? await User.select().count() : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAccountApproval(req: Request, res: Response): Promise<void> {
|
||||||
|
const accounts = await User.select().where('approved', 0).with('mainEmail').get();
|
||||||
|
res.render('backend/accounts_approval', {
|
||||||
|
accounts: User.isApprovalMode() ? accounts : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getApproveAccount(req: Request, res: Response): Promise<void> {
|
||||||
|
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
|
||||||
|
if (!account) throw new NotFoundHttpError('User', req.url);
|
||||||
|
const email = await account.mainEmail.get();
|
||||||
|
|
||||||
|
account.approved = true;
|
||||||
|
await account.save();
|
||||||
|
|
||||||
|
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
|
approved: true,
|
||||||
|
link: config.get<string>('base_url') + Controller.route('auth'),
|
||||||
|
}).send(email!.email!);
|
||||||
|
|
||||||
|
req.flash('success', `Account successfully approved.`);
|
||||||
|
res.redirectBack(Controller.route('accounts-approval'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRejectAccount(req: Request, res: Response): Promise<void> {
|
||||||
|
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
|
||||||
|
if (!account) throw new NotFoundHttpError('User', req.url);
|
||||||
|
const email = await account.mainEmail.get();
|
||||||
|
|
||||||
|
await account.delete();
|
||||||
|
|
||||||
|
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
|
approved: false,
|
||||||
|
}).send(email!.email!);
|
||||||
|
|
||||||
|
req.flash('success', `Account successfully deleted.`);
|
||||||
|
res.redirectBack(Controller.route('accounts-approval'));
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
|
|||||||
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.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.get('/files/delete/:slug', this.deleteFile, 'delete-file-frontend', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
@ -30,10 +31,14 @@ export default class FileController extends Controller {
|
|||||||
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
protected async getFileUploader(req: Request, res: Response): Promise<void> {
|
||||||
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.getForUser(req.models.user!.id!),
|
auth_tokens: await AuthToken.select().where('user_id', req.models.user!.id!),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async downloadLinuxScript(req: Request, res: Response): Promise<void> {
|
||||||
|
res.download('assets/files/upload_file.sh', 'upload_file.sh');
|
||||||
|
}
|
||||||
|
|
||||||
protected async getFileManager(req: Request, res: Response): Promise<void> {
|
protected async getFileManager(req: Request, res: Response): Promise<void> {
|
||||||
res.render('file-manager', {
|
res.render('file-manager', {
|
||||||
files: await FileModel.paginateForUser(req, 100, req.models.user!.id!),
|
files: await FileModel.paginateForUser(req, 100, req.models.user!.id!),
|
||||||
@ -54,7 +59,7 @@ export default class FileController extends Controller {
|
|||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
if (!id) throw new BadRequestError('Cannot revoke token without an id.', 'Please provide an id.', req.url);
|
if (!id) throw new BadRequestError('Cannot revoke token without an id.', 'Please provide an id.', req.url);
|
||||||
|
|
||||||
const authToken = await AuthToken.getById<AuthToken>(`${id}`);
|
const authToken = await AuthToken.getById<AuthToken>(parseInt(id));
|
||||||
if (!authToken) throw new NotFoundHttpError('Auth token', req.url);
|
if (!authToken) throw new NotFoundHttpError('Auth token', req.url);
|
||||||
if (!authToken.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('auth token', req.url);
|
if (!authToken.canDelete(req.models.user!.id!)) throw new ForbiddenHttpError('auth token', req.url);
|
||||||
|
|
||||||
|
@ -15,12 +15,14 @@ export default class MagicLinkController extends _MagicLinkController {
|
|||||||
switch (magicLink.getActionType()) {
|
switch (magicLink.getActionType()) {
|
||||||
case MagicLinkActionType.LOGIN:
|
case MagicLinkActionType.LOGIN:
|
||||||
case MagicLinkActionType.REGISTER:
|
case MagicLinkActionType.REGISTER:
|
||||||
await AuthController.checkAndAuth(req, magicLink);
|
await AuthController.checkAndAuth(req, res, magicLink);
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
// Auth success
|
// Auth success
|
||||||
const user = await req.authGuard.getUserForSession(req.session!);
|
const user = await req.authGuard.getUserForSession(req.session!);
|
||||||
req.flash('success', `Authentication success. Welcome, ${user?.name}!`);
|
req.flash('success', `Authentication success. Welcome, ${user?.name}!`);
|
||||||
res.redirect(Controller.route('home'));
|
res.redirect(Controller.route('home'));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,6 @@ import Validator from "wms-core/db/Validator";
|
|||||||
import {cryptoRandomDictionary} from "wms-core/Utils";
|
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||||
|
|
||||||
export default class AuthToken extends Model implements AuthProof {
|
export default class AuthToken extends Model implements AuthProof {
|
||||||
public static async getBySecret(secret: string): Promise<AuthToken | null> {
|
|
||||||
const models = await this.models<AuthToken>(this.select().where('secret', secret).first());
|
|
||||||
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 user_id!: number;
|
||||||
protected readonly secret!: string;
|
protected readonly secret!: string;
|
||||||
protected created_at?: Date;
|
protected created_at?: Date;
|
||||||
@ -30,12 +21,12 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected defineProperties() {
|
protected init() {
|
||||||
this.defineProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||||
this.defineProperty('secret', new Validator().defined().between(32, 64));
|
this.addProperty('secret', new Validator().defined().between(32, 64));
|
||||||
this.defineProperty('created_at', new Validator());
|
this.addProperty('created_at', new Validator());
|
||||||
this.defineProperty('used_at', new Validator());
|
this.addProperty('used_at', new Validator());
|
||||||
this.defineProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
|
this.addProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
|
||||||
}
|
}
|
||||||
|
|
||||||
public use() {
|
public use() {
|
||||||
@ -53,13 +44,13 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getEmail(): Promise<string> {
|
public async getEmail(): Promise<string> {
|
||||||
let userEmail = await UserEmail.getMainFromUser(this.user_id);
|
let userEmail = await UserEmail.select().where('user_id', this.user_id).first();
|
||||||
if (!userEmail) throw new Error("Cannot find main user email for user " + this.user_id);
|
if (!userEmail) throw new Error("Cannot find main user email for user " + this.user_id);
|
||||||
return userEmail.email;
|
return userEmail.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUser(): Promise<User | null> {
|
public async getUser(): Promise<User | null> {
|
||||||
return await User.getById<User>(`${this.user_id}`);
|
return await User.getById<User>(this.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isAuthorized(): Promise<boolean> {
|
public async isAuthorized(): Promise<boolean> {
|
||||||
|
@ -11,8 +11,7 @@ export default class FileModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getBySlug(slug: string): Promise<FileModel | null> {
|
public static async getBySlug(slug: string): Promise<FileModel | null> {
|
||||||
const models = await this.models<FileModel>(this.select().where('slug', slug).first());
|
return await this.select().where('slug', slug).first();
|
||||||
return models.length > 0 ? models[0] : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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[]> {
|
||||||
@ -28,15 +27,15 @@ export default class FileModel extends Model {
|
|||||||
public created_at?: Date;
|
public created_at?: Date;
|
||||||
public readonly ttl!: number;
|
public readonly ttl!: number;
|
||||||
|
|
||||||
protected defineProperties() {
|
protected init() {
|
||||||
this.defineProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||||
this.defineProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
||||||
this.defineProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
this.addProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
||||||
this.defineProperty('storage_type', new Validator().defined().maxLength(64));
|
this.addProperty('storage_type', new Validator().defined().maxLength(64));
|
||||||
this.defineProperty('storage_path', new Validator().defined().maxLength(1745));
|
this.addProperty('storage_path', new Validator().defined().maxLength(1745));
|
||||||
this.defineProperty('size', new Validator().defined().min(0));
|
this.addProperty('size', new Validator().defined().min(0));
|
||||||
this.defineProperty('created_at', new Validator());
|
this.addProperty('created_at', new Validator());
|
||||||
this.defineProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
this.addProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(): string {
|
public getURL(): string {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
{% set title = 'ily.li - About' %}
|
{% set title = app.name + ' - About us' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>Self-hosting files</h1>
|
<h1>Self-hosting files</h1>
|
||||||
|
41
views/backend/accounts_approval.njk
Normal file
41
views/backend/accounts_approval.njk
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - Review accounts' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Accounts pending review</h1>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="shrink-col">#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Main email</th>
|
||||||
|
<th>Registered at</th>
|
||||||
|
<th class="shrink-col">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in accounts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ user.mainEmail.getOrFail().email }}</td>
|
||||||
|
<td>{{ user.created_at.toISOString() }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="max-content">
|
||||||
|
<a href="{{ route('approve-account', user.id) }}"
|
||||||
|
class="button success"><i data-feather="check"></i> Approve</a>
|
||||||
|
|
||||||
|
<a href="{{ route('reject-account', user.id) }}"
|
||||||
|
onclick="return confirm(`This will irrevocably delete the ${user.main_email} account.`)"
|
||||||
|
class="button danger"><i data-feather="x"></i> Reject</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
21
views/backend/index.njk
Normal file
21
views/backend/index.njk
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - Backend' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>App administration</h1>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
{% if approval_mode %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('accounts-approval') }}">Accounts approval ({{ accounts_to_approve }})</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -7,7 +7,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block _body %}
|
{% block _body %}
|
||||||
<div class="logo"><a href="/">Example app</a></div>
|
<div class="logo"><a href="/">{{ app.name }}</a></div>
|
||||||
|
|
||||||
<main class="{% block class %}{% endblock %}">
|
<main class="{% block class %}{% endblock %}">
|
||||||
{% if flash %}
|
{% if flash %}
|
||||||
@ -31,6 +31,6 @@
|
|||||||
Error ID: {{ error_id }}
|
Error ID: {{ error_id }}
|
||||||
<br>
|
<br>
|
||||||
If you think this isn't right, please contact us with the above error ID at
|
If you think this isn't right, please contact us with the above error ID at
|
||||||
<a href="mailto:contact@example.net">contact@example.net</a>.
|
<a href="mailto:{{ app.contact_email }}">{{ app.contact_email }}</a>.
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,6 +1,6 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
{% set title = 'ily.li - File manager' %}
|
{% set title = app.name + ' - File manager' %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/js/fm.js"></script>
|
<script src="/js/fm.js"></script>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
{% set title = 'ily.li - File manager' %}
|
{% set title = app.name + ' - File upload' %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/js/fm.js"></script>
|
<script src="/js/fm.js"></script>
|
||||||
@ -46,13 +46,29 @@
|
|||||||
<h2>Setup a desktop utility</h2>
|
<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
|
<p>There may be a desktop client at some point. For now, if you're an advanced user, you can setup
|
||||||
scripts/macros.</p>
|
scripts/macros.</p>
|
||||||
|
|
||||||
|
<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>
|
<p>
|
||||||
To upload the file, you must:
|
To upload the file, you must:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Set the "Authorization" HTTP header to an auth token (generate one with the form below)</li>
|
<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
|
<li>Make a proper file upload request either with the method "POST" on / (auto-generates a short
|
||||||
"PUT" (choose the target url you want, alphanum)
|
url) or "PUT" (choose the target url you want, alphanum)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
@ -96,6 +112,7 @@ https://ily.li/my_very_important_file.png</pre>
|
|||||||
-F 'ttl=30' \
|
-F 'ttl=30' \
|
||||||
https://ily.li/</pre>
|
https://ily.li/</pre>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@ -130,8 +147,7 @@ https://ily.li/</pre>
|
|||||||
<td>{{ token.created_at.toISOString() }}</td>
|
<td>{{ token.created_at.toISOString() }}</td>
|
||||||
<td>{{ token.used_at.toISOString() }}</td>
|
<td>{{ token.used_at.toISOString() }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('revoke-token', token.id) }}" class="button danger"><i
|
<a href="{{ route('revoke-token', token.id) }}" class="button danger"><i data-feather="trash"></i> Revoke</a>
|
||||||
data-feather="trash"></i> Revoke</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -12,13 +12,16 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> ily.li</a>
|
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {{ app.name }}</a>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> About</a></li>
|
<li><a href="{{ route('about') }}"><i data-feather="info"></i> About</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-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('file-manager') }}"><i data-feather="folder"></i> File manager</a></li>
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> Backend</a></li>
|
||||||
|
{% 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> Logout</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> Login / Register</a></li>
|
||||||
@ -44,4 +47,4 @@
|
|||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}ily.li v{{ app_version }} - all rights reserved.{% endblock %}
|
{% block footer %}{{ app.name }} v{{ app_version }} - all rights reserved.{% endblock %}
|
41
views/mails/account_review_notice.mjml.njk
Normal file
41
views/mails/account_review_notice.mjml.njk
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends './base_layout.mjml.njk' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="title">
|
||||||
|
{% if approved %}
|
||||||
|
Your registration was approved!
|
||||||
|
{% else %}
|
||||||
|
Sorry, your registration was rejected.
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text>
|
||||||
|
{% if approved %}
|
||||||
|
An administrator approved your registration. You can now log in to your account.
|
||||||
|
{% else %}
|
||||||
|
Your registration was rejected and your account was deleted from our database.
|
||||||
|
If you believe that this is an error, please contact us via email.
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
{% if approved %}
|
||||||
|
<mj-button href="{{ link | safe }}">Login</mj-button>
|
||||||
|
{% endif %}
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block text %}
|
||||||
|
{% if approved %}
|
||||||
|
Hi
|
||||||
|
Your registration was approved!
|
||||||
|
|
||||||
|
You can now log in to your account by follwing this link: {{ link|safe }}
|
||||||
|
{% else %}
|
||||||
|
Hi
|
||||||
|
Sorry, your registration was rejected. Your account was deleted from our database.
|
||||||
|
|
||||||
|
If you believe that this is an error, please contact us via email.
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -93,7 +93,7 @@
|
|||||||
<mj-section mj-class="header">
|
<mj-section mj-class="header">
|
||||||
<mj-column>
|
<mj-column>
|
||||||
<mj-text mj-class="header">
|
<mj-text mj-class="header">
|
||||||
ily.li
|
{{ app.name }}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
</mj-section>
|
</mj-section>
|
||||||
@ -104,7 +104,7 @@
|
|||||||
<mj-column>
|
<mj-column>
|
||||||
<mj-text mj-class="footer">
|
<mj-text mj-class="footer">
|
||||||
All rights reserved. Contact us at
|
All rights reserved. Contact us at
|
||||||
<a href="mailto:(contact email)" class="link">(contact email)</a>
|
<a href="mailto:{{ app.contact_email }}" class="link">{{ app.contact_email }}</a>
|
||||||
</mj-text>
|
</mj-text>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
</mj-section>
|
</mj-section>
|
||||||
|
66
views/mails/pending_account_review.mjml.njk
Normal file
66
views/mails/pending_account_review.mjml.njk
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{% extends './base_layout.mjml.njk' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="title">
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Register an account on (app)
|
||||||
|
{% else %}
|
||||||
|
Log in to (app)
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text>
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Someone has requested an account registration for <strong>{{ mail_to }}</strong>. If it was not you,
|
||||||
|
please ignore this message.
|
||||||
|
{% else %}
|
||||||
|
Someone is attempting to log in to your account <strong>{{ mail_to }}</strong>.
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
{% if type == 'register' %}
|
||||||
|
<mj-button href="{{ link | safe }}">Finalize my account registration</mj-button>
|
||||||
|
{% else %}
|
||||||
|
<mj-text>If it is not you, <strong>DO NOT CLICK ON THIS BUTTON</strong>.</mj-text>
|
||||||
|
{% endif %}
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
{% if type == 'login' %}
|
||||||
|
<mj-section background-color="#1b252d">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="important-line" padding-bottom="0px">
|
||||||
|
IP: <strong>{{ ip }}</strong>
|
||||||
|
</mj-text>
|
||||||
|
<mj-text mj-class="important-line">
|
||||||
|
Location: <strong>{{ geo }}</strong>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
<mj-column>
|
||||||
|
<mj-button href="{{ link | safe }}" padding="20px 0" background-color="#caa200">
|
||||||
|
Authorize log in
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block text %}
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Hi!
|
||||||
|
Someone requested an account registration for {{ mail_to }}. If it was not you,
|
||||||
|
please ignore this message.
|
||||||
|
|
||||||
|
To finalize your account registration, please follow this link: {{ link|safe }}
|
||||||
|
{% else %}
|
||||||
|
Hi!
|
||||||
|
Someone is attempting to log in to your account {{ mail_to }}.
|
||||||
|
|
||||||
|
If it is not you, DO NOT FOLLOW THIS LINK.
|
||||||
|
|
||||||
|
IP: {{ ip }}
|
||||||
|
Location: {{ geo }}
|
||||||
|
To authorize this log in, please follow this link: {{ link|safe }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user