Move all sources to src folder, add common ts subproject

This commit is contained in:
Alice Gaudon 2021-05-27 15:26:19 +02:00
parent c36e4c0fbe
commit c6b8c48a72
49 changed files with 143 additions and 428 deletions

View File

@ -10,9 +10,11 @@ module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: [
'./tsconfig.json',
'./tsconfig.test.json',
'./tsconfig.frontend.json',
'./src/tsconfig.json',
'./src/common/tsconfig.json',
'./src/assets/ts/tsconfig.json',
'./src/assets/views/tsconfig.json',
]
},
extends: [

4
.gitignore vendored
View File

@ -1,8 +1,10 @@
.idea
node_modules
build
dist
intermediates
public
yarn-error.log
config/local.*
tsconfig.tsbuildinfo

View File

@ -1,112 +0,0 @@
{% extends 'layouts/base.njk' %}
{% import 'macros.njk' as macros %}
{% set title = 'Authentication / Registration' %}
{% set decription = 'Join ' + app.name + ' and share your files!' %}
{% set h1 = 'Authentication and registration' %}
{% block body %}
<div class="container">
{% set queryStr = '' %}
{% set previousUrl = getPreviousUrl() %}
{% if query.redirect_uri | length %}
{% set queryStr = '?' + querystring.stringify({redirect_uri: query.redirect_uri}) %}
{% elif previousUrl | length %}
{% set queryStr = '?' + querystring.stringify({redirect_uri: previousUrl}) %}
{% endif %}
<section class="panel">
<h2><i data-feather="log-in"></i> Log in</h2>
{{ setFormPrefix('login-') }}
<form action="{{ route('login') + queryStr }}" method="POST" id="login-form">
{{ macros.field(_locals, 'text', 'identifier', query.identifier or '', 'Your email address or username', null, 'required') }}
{{ macros.field(_locals, 'password', 'password', null, 'Your password', 'Do not fill to log in via magic link.') }}
{{ macros.field(_locals, 'checkbox', 'persist_session', null, 'Stay logged in on this computer.') }}
<button type="submit">Authenticate</button>
{{ macros.csrf(getCsrfToken) }}
</form>
</section>
<section class="panel">
<h2><i data-feather="user-plus"></i> Register</h2>
{{ setFormPrefix('register-') }}
<form action="{{ route('register') + queryStr }}" method="POST" id="register-form">
<input type="hidden" name="auth_method" value="magic_link">
{{ macros.csrf(getCsrfToken) }}
{% if has_username %}
{{ macros.field(_locals, 'text', 'name', null, 'Choose your username', 'This cannot be changed later.', 'pattern="[0-9a-z_-]+" required') }}
{% endif %}
<div id="register-magic_link_method_fields">
{{ macros.field(_locals, 'email', 'identifier', null, 'Your email address', null, 'required') }}
<a href="javascript: void(0);">Use password instead</a>
</div>
<div id="register-password_method_fields" class="hidden">
{{ macros.field(_locals, 'password', 'password', null, 'Choose a password', null, 'required disabled') }}
{{ macros.field(_locals, 'password', 'password_confirmation', null, 'Confirm your password', null, 'required disabled') }}
<a href="javascript: void(0);">Use email address instead</a>
</div>
{{ macros.field(_locals, 'checkbox', 'terms', null, 'I accept the <a href="/terms-of-services" target="_blank">Terms Of Services</a>.' | safe, null, 'required') }}
<button type="submit" class="primary">Register</button>
</form>
</section>
</div>
{% endblock %}
{% block scripts %}
<script>
// Register form dynamics
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('register-form');
const authMethodField = form.querySelector('input[name=auth_method]');
const usernameField = form.querySelector('input[name=name]');
const magicLinkFields = document.getElementById('register-magic_link_method_fields');
const passwordFields = document.getElementById('register-password_method_fields');
let switchToPassword;
magicLinkFields.querySelector('a').addEventListener('click', switchToPassword = () => {
authMethodField.value = 'password';
usernameField.name = 'identifier';
magicLinkFields.classList.add('hidden');
magicLinkFields.querySelectorAll('input').forEach(el => {
el.disabled = true;
});
passwordFields.classList.remove('hidden');
passwordFields.querySelectorAll('input').forEach(el => {
el.disabled = false;
});
});
passwordFields.querySelector('a').addEventListener('click', () => {
authMethodField.value = 'magic_link';
usernameField.name = 'name';
magicLinkFields.classList.remove('hidden');
magicLinkFields.querySelectorAll('input').forEach(el => {
el.disabled = false;
});
passwordFields.classList.add('hidden');
passwordFields.querySelectorAll('input').forEach(el => {
el.disabled = true;
});
});
if (`{{ _locals.previousFormData()['auth_method'] | default('') }}` === 'password') {
switchToPassword();
}
});
</script>
{% endblock %}

View File

@ -1,185 +0,0 @@
{% macro message(type, content, raw=false, discreet=false) %}
<div class="message{{ ' message-discreet' if discreet }}" data-type="{{ type }}">
<i class="icon"></i>
<span class="content">
{{ content|safe if raw else content }}
</span>
</div>
{% endmacro %}
{% macro messages(flash) %}
{% set flashed = flash() %}
{% set display = 0 %}
{% for type, bag in flashed %}
{% if bag|length %}
{% set display = 1 %}
{% endif %}
{% endfor %}
{% if display %}
<div class="messages">
{% for type, bag in flashed %}
{% for content in bag %}
{{ message(type, content) }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro csrf(getCsrfToken) %}
<input type="hidden" name="csrf" value="{{ getCsrfToken() }}">
{% endmacro %}
{% macro field(_locals, type, name, value, placeholder, hint, validation_attributes='', extraData='', icon=null) %}
{% set validation = _locals.validation() %}
{% set validation = validation[name] if validation[name] or null %}
{% set previousFormData = _locals.previousFormData() %}
{% set value = previousFormData[name] or value or validation.value or '' %}
{% set prefix = _locals.getFormPrefix() | default('') %}
{% if type == 'hidden' %}
{% if validation %}
{{ message('error', validation.message) }}
{% endif %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% else %}
<div class="form-field{{ ' inline' if type == 'checkbox' }}">
<div class="control">
{% if icon != null %}
{% if icon.startsWith('fa') %}
<i class="{{ icon }} feather icon"></i>
{% else %}
<i data-feather="{{ icon }}" class="icon"></i>
{% endif %}
{% endif %}
{% if type == 'duration' %}
<div class="input-group">
{% for f in extraData %}
<div class="time-input">
{% if previousFormData[name] %}
{% set v = value[f] %}
{% else %}
{% set v = (value % 60) if f == 's' else (((value - value % 60) / 60 % 60) if f == 'm' else ((value - value % 3600) / 3600 if f == 'h')) %}
{% endif %}
<input type="number" name="{{ name }}[{{ f }}]" id="field-{{ prefix }}{{ name }}-{{ f }}"
value="{{ v }}"
min="0" {{ 'max=60' if (f == 's' or f == 'm') }}
{{ validation_attributes }}>
<label for="field-{{ prefix }}{{ name }}-{{ f }}">{{ f }}</label>
</div>
{% endfor %}
</div>
{% elseif type == 'select' %}
<select name="{{ name }}" id="field-{{ prefix }}{{ name }}" {{ validation_attributes|safe }}>
{% for option in extraData %}
<option value="{% if option.display === undefined or option.value !== undefined %}{{ option.value | default(option) }}{% endif %}"
{{ 'selected' if value == (option.value | default(option)) }}>{{ option.display | default(option) }}</option>
{% endfor %}
</select>
<i data-feather="chevron-down"></i>
{% elseif type == 'textarea' %}
<textarea name="{{ name }}" id="field-{{ prefix }}{{ name }}"
{{ validation_attributes|safe }} value="{{ value }}">{{ value }}</textarea>
{% else %}
<input type="{{ type }}" name="{{ name }}" id="field-{{ prefix }}{{ name }}"
{% if type != 'checkbox' %} value="{{ value }}" {% endif %}
{{ 'checked' if (type == 'checkbox' and value == 'on') }}
{{ validation_attributes|safe }}>
{% endif %}
<label for="field-{{ prefix }}{{ name }}{{ '-' + extraData[0] if type == 'duration' }}">{{ placeholder }}</label>
</div>
{{ fieldError(_locals, name) }}
{% if hint %}
<div class="hint"><i data-feather="info"></i> {{ hint }}</div>
{% endif %}
</div>
{% endif %}
{% endmacro %}
{% macro fieldError(_locals, name) %}
{% set validation = _locals.validation() %}
{% set validation = validation[name] if validation[name] or null %}
{% if validation %}
<div class="error"><i data-feather="x-circle"></i> {{ validation.message }}</div>
{% endif %}
{% endmacro %}
{% macro websocket(websocketUrl, listener, reconnectOnClose = 1, checkFunction = 0) %}
<script>
document.addEventListener('DOMContentLoaded', () => {
{% if checkFunction %}
if (!{{ checkFunction }}()) return;
{% endif %}
const run = () => {
const websocket = new WebSocket('{{ websocketUrl }}');
websocket.onopen = (e) => {
console.debug('Websocket connected');
};
websocket.onmessage = (e) => {
{{ listener }}(websocket, e);
};
websocket.onerror = (e) => {
console.error('Websocket error', e);
};
websocket.onclose = (e) => {
console.debug('Websocket closed', e.code, e.reason);
{% if reconnectOnClose %}
setTimeout(run, 1000);
{% endif %}
};
};
run();
});
</script>
{% endmacro %}
{% macro paginate(pagination, routeName, contextSize) %}
{% if pagination.hasPrevious() or pagination.hasNext() %}
<nav class="pagination">
<ul>
{% if pagination.hasPrevious() %}
<li><a href="{{ route(routeName, {page: pagination.page - 1}) }}"><i data-feather="chevron-left"></i> Previous</a></li>
{% for i in pagination.previousPages(contextSize) %}
{% if i == -1 %}
<li class="ellipsis">...</li>
{% else %}
<li><a href="{{ route(routeName, {page: i}) }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
<li class="active"><span>{{ pagination.page }}</span></li>
{% if pagination.hasNext() %}
{% for i in pagination.nextPages(contextSize) %}
{% if i == -1 %}
<li class="ellipsis">...</li>
{% else %}
<li><a href="{{ route(routeName, {page: i}) }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
<li><a href="{{ route(routeName, {page: pagination.page + 1}) }}">Next <i data-feather="chevron-right"></i></a></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endmacro %}
{% macro breadcrumb(currentPageTitle, pages=[]) %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for page in pages %}
<li><a href="{{ page.link }}">{{ page.title }}</a></li>
{% endfor %}
<li class="active" aria-current="page">{{ currentPageTitle }}</li>
</ol>
</nav>
{% endmacro %}

View File

@ -1,57 +0,0 @@
{% extends 'layouts/base.njk' %}
{% set h1 = 'Authentication lobby' %}
{% set title = app.name + ' ' + h1 %}
{% block body %}
<div class="container">
<div class="panel">
{{ macros.message('success', 'We sent a link to ' + email + '. To authenticate, open it from any device.') }}
{{ macros.message('info', 'This link will be valid for <span id="countdown"></span> and can only be used once.', true, true) }}
<p class="center">Waiting for you to open the link...</p>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const validUntil = {{ validUntil }}.0;
function isValid() {
return new Date().getTime() < validUntil;
}
function websocketListen(websocket, e) {
if (e.data === 'refresh') {
window.location.reload();
}
}
document.addEventListener('DOMContentLoaded', () => {
const countdown = document.getElementById('countdown');
if (!isValid()) return;
function animateCountdown() {
requestAnimationFrame(() => {
let diff = Math.max(0, validUntil - new Date().getTime());
if (diff === 0) {
return;
}
diff /= 1000;
const seconds = Math.floor(diff % 60).toFixed(0);
const minutes = Math.floor((diff - seconds) / 60).toFixed(0);
countdown.innerText = `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
animateCountdown();
});
}
animateCountdown();
});
</script>
{{ macros.websocket(websocketUrl, 'websocketListen', 1, 'isValid') }}
{% endblock %}

View File

@ -17,10 +17,10 @@
"test": "jest --verbose --runInBand",
"clean": "node scripts/clean.js",
"prepare-sources": "node scripts/prepare-sources.js",
"compile": "yarn clean && tsc",
"build": "yarn prepare-sources && yarn compile && node . pre-compile-views && node scripts/dist.js",
"compile": "yarn clean && yarn prepare-sources && tsc --build",
"build": "yarn compile && node . pre-compile-views && node scripts/dist.js",
"build-production": "NODE_ENV=production yarn build",
"dev": "yarn compile && yarn prepare-sources && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --watch\" \"nodemon -i public -i build -- pre-compile-views --watch\" \"nodemon -i public -i build\"",
"dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
"lint": "eslint .",
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
},

18
scripts/_functions.js Normal file
View File

@ -0,0 +1,18 @@
import fs from "fs";
import path from "path";
export function copyRecursively(file, destination) {
const target = path.join(destination, path.basename(file));
if (fs.statSync(file).isDirectory()) {
console.log('mkdir', target);
fs.mkdirSync(target, {recursive: true});
fs.readdirSync(file).forEach(f => {
copyRecursively(path.join(file, f), target);
});
} else {
console.log('> cp ', target);
fs.copyFileSync(file, target);
}
}

View File

@ -1,7 +1,7 @@
import fs from "fs";
[
'build',
'intermediates',
'dist',
'public',
].forEach(file => {

View File

@ -1,28 +1,12 @@
import fs from "fs";
import path from "path";
function copyRecursively(file, destination) {
const target = path.join(destination, path.basename(file));
if (fs.statSync(file).isDirectory()) {
console.log('mkdir', target);
fs.mkdirSync(target, {recursive: true});
fs.readdirSync(file).forEach(f => {
copyRecursively(path.join(file, f), target);
});
} else {
console.log('> cp ', target);
fs.copyFileSync(file, target);
}
}
import {copyRecursively} from "./_functions.js";
[
'yarn.lock',
'README.md',
'config/',
'assets/',
].forEach(file => {
copyRecursively(file, 'dist');
});

View File

@ -1,9 +1,24 @@
import fs from "fs";
import path from "path";
import {copyRecursively} from "./_functions.js";
// These folders must exist for nodemon not to loop indefinitely.
[
'build',
'public',
'dist',
'intermediates',
'intermediates/assets',
].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
});
// Symlink to build/common
const symlink = path.resolve('intermediates/common');
if (!fs.existsSync(symlink)) {
fs.symlinkSync(path.resolve('dist/common'), symlink);
}
// Copy all source files
fs.readdirSync('src').forEach(file => {
copyRecursively(path.join('src', file), 'dist');
});

View File

@ -84,7 +84,7 @@ export default class TestApp extends Application {
this.use(new MaintenanceComponent());
// Dynamic views and routes
const intermediateDirectory = 'build';
const intermediateDirectory = 'intermediates/assets';
this.use(new FrontendToolsComponent(
new AssetCompiler(intermediateDirectory, 'public'),
new CopyAssetPreCompiler(intermediateDirectory, '', 'json', ['test/assets'], false),
@ -96,7 +96,7 @@ export default class TestApp extends Application {
// Services
this.use(new MysqlComponent());
this.use(new MailComponent(new MailViewEngine('build', 'test/assets')));
this.use(new MailComponent(new MailViewEngine('intermediates/assets', 'test/assets')));
// Session
this.use(new RedisComponent());

View File

@ -23,11 +23,11 @@
</div>
{% if has_name_component %}
{% include './name_panel.njk' %}
{% include 'views/auth/account/name_panel.njk' %}
{% endif %}
{% if has_password_component %}
{% include './password_panel.njk' %}
{% include 'views/auth/account/password_panel.njk' %}
{% endif %}
<section class="panel">

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {locals} from "../../ts/stores";
import {locals} from "../../ts/stores.js";
const previousUrl = $locals.getPreviousUrl();

View File

@ -9,13 +9,11 @@
import * as stores from '/js/stores.js';
const localStore = stores[Object.keys(stores)[0]].locals;
const locals = %locals%;
const localMap = %locals%;
localStore.set((key, args) => {
const localKey = args ?
return localMap[args ?
`'${key}', \`${args}\``
: `'${key}'`;
// console.debug(localKey + '=' + locals[localKey])
return locals[localKey];
: `'${key}'`];
});
new View({

View File

@ -0,0 +1,15 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"outDir": "public/js",
"rootDir": "../../../intermediates/assets",
},
"include": [
"src/assets/ts/**/*"
],
"references": [
{
"path": "../../common"
}
]
}

View File

@ -1,13 +1,13 @@
import config from "config";
import {Request, Response} from "express";
import Time from "../common/Time.js";
import Controller from "../Controller.js";
import ModelFactory from "../db/ModelFactory.js";
import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator.js";
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "../HttpError.js";
import MailTemplate from "../mail/MailTemplate.js";
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails.js";
import Time from "../Time.js";
import {RequireAuthMiddleware} from "./AuthComponent.js";
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType.js";
import MagicLinkController from "./magic_link/MagicLinkController.js";

3
src/common/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

20
src/common/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "CommonJS",
"baseUrl": "../../dist/common",
"rootDir": "./",
"sourceRoot": "./",
"outDir": "../../dist/common",
"typeRoots": [
"src/types"
],
},
"include": [
"./**/*"
]
}

View File

@ -34,7 +34,7 @@ export default abstract class AssetPreCompiler {
) {
this.assetPaths = [
...additionalFallbackAssetPaths,
'assets',
'src/assets',
'node_modules/swaf/assets',
].map(p => path.resolve(p, assetType))
.filter(dir => existsSync(dir));

View File

@ -1,4 +1,4 @@
import "../../node_modules/svelte/register.js";
import "./register_svelte/register_svelte.js";
import clearModule from "clear-module";
import config from "config";
@ -176,7 +176,7 @@ export default class SvelteViewEngine extends ViewEngine {
replacedBackendCalls.code,
sveltePreprocess({
typescript: {
tsconfigFile: 'tsconfig.svelte.json',
tsconfigFile: 'src/assets/views/tsconfig.json',
},
}),
{
@ -276,7 +276,7 @@ export default class SvelteViewEngine extends ViewEngine {
});
// Load locals into locals store
const localsModulePath = "../../build/ts/stores.js";
const localsModulePath = "../../intermediates/assets/ts/stores.js";
const localsModule = await import(localsModulePath);
const locals = this.getGlobals().get();
const localMap = this.compileBackendCalls(backendCalls, locals, true);

View File

@ -12,7 +12,7 @@ export default class TypeScriptPreCompiler extends CopyAssetPreCompiler {
this.onPreCompile(async _watch => {
logger.info('Building ts assets...');
await child_process.execSync(`yarn tsc -b tsconfig.frontend.json`, {stdio: [process.stdin, process.stdout, process.stderr]});
await child_process.execSync(`yarn tsc -b src/assets/ts/tsconfig.json`, {stdio: [process.stdin, process.stdout, process.stderr]});
});
}
}

View File

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@ -0,0 +1 @@
require("svelte/register");

27
src/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"baseUrl": "../dist",
"rootDir": "./",
"sourceRoot": "./",
"outDir": "../dist",
"typeRoots": [
"src/types"
]
},
"include": [
"./**/*"
],
"exclude": [
"./assets/**/*",
"./common/**/*"
],
"references": [
{
"path": "./common"
}
]
}

View File

@ -1,18 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"baseUrl": "build",
"rootDir": "build/ts-source",
"sourceRoot": "build/ts-source",
"outDir": "build/ts",
"declaration": false,
"typeRoots": [],
"resolveJsonModule": false
},
"include": [
"build/ts-source/**/*"
]
}

View File

@ -7,12 +7,11 @@
"strict": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"moduleResolution": "Node",
"esModuleInterop": true,
"baseUrl": "dist",
"rootDir": "src",
"sourceRoot": "src",
"inlineSourceMap": true,
"inlineSources": true,
"outDir": "dist",
@ -25,9 +24,19 @@
"es2020"
],
"resolveJsonModule": true,
"skipLibCheck": true
"skipLibCheck": true,
"allowJs": true,
},
"include": [
"src/**/*"
"include": [],
"references": [
{
"path": "src",
},
{
"path": "src/assets/ts",
},
{
"path": "src/assets/views",
}
]
}

View File

@ -1,10 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"outDir": "public/js",
"rootDir": "build",
},
"include": [
"assets/ts/**/*"
],
}