diff --git a/.gitignore b/.gitignore index eb79dd5..700344c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules .idea +public +dist \ No newline at end of file diff --git a/assets/config.json b/assets/config.json new file mode 100644 index 0000000..5c18915 --- /dev/null +++ b/assets/config.json @@ -0,0 +1,10 @@ +{ + "bundles": { + "app": "js/app.js", + "layout": "sass/layout.scss", + "error": "sass/error.scss", + "logo": "img/logo.svg", + "logo_png": "img/logox128.png", + "logo_png_xxl": "img/logox1024.png" + } +} \ No newline at end of file diff --git a/assets/img/logo.svg b/assets/img/logo.svg new file mode 100644 index 0000000..e81b818 --- /dev/null +++ b/assets/img/logo.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/assets/img/logox1024.png b/assets/img/logox1024.png new file mode 100644 index 0000000..f5894c0 Binary files /dev/null and b/assets/img/logox1024.png differ diff --git a/assets/img/logox128.png b/assets/img/logox128.png new file mode 100644 index 0000000..a1252a5 Binary files /dev/null and b/assets/img/logox128.png differ diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..fc7c539 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,6 @@ +import './external_links'; +import './message_icons'; + +import '../sass/app.scss'; + +console.log('Hello world!'); \ No newline at end of file diff --git a/assets/js/external_links.js b/assets/js/external_links.js new file mode 100644 index 0000000..7108c4e --- /dev/null +++ b/assets/js/external_links.js @@ -0,0 +1,9 @@ +import feather from "feather-icons"; + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('a[target="_blank"]').forEach(el => { + el.innerHTML += ``; + }); + + feather.replace(); +}); \ No newline at end of file diff --git a/assets/js/message_icons.js b/assets/js/message_icons.js new file mode 100644 index 0000000..b625f55 --- /dev/null +++ b/assets/js/message_icons.js @@ -0,0 +1,21 @@ +import feather from "feather-icons"; + +document.addEventListener('DOMContentLoaded', () => { + const messageTypeToIcon = { + info: 'info', + success: 'check', + warning: 'alert-triangle', + error: 'x-circle', + question: 'help-circle', + }; + document.querySelectorAll('.message').forEach(el => { + const type = el.dataset['type']; + const icon = el.querySelector('.icon'); + const svgContainer = document.createElement('div'); + svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg(); + el.insertBefore(svgContainer.firstChild, icon); + icon.remove(); + }); + + feather.replace(); +}); \ No newline at end of file diff --git a/assets/sass/_fonts.scss b/assets/sass/_fonts.scss new file mode 100644 index 0000000..6c26b55 --- /dev/null +++ b/assets/sass/_fonts.scss @@ -0,0 +1,81 @@ +/* vietnamese */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5iU1EQVg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5jU1EQVg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5tU1E.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cceyI9tScg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8ccezI9tScg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cce9I9s.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5iU1EQVg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5jU1EQVg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Nunito Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5tU1E.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/assets/sass/_vars.scss b/assets/sass/_vars.scss new file mode 100644 index 0000000..5c7b523 --- /dev/null +++ b/assets/sass/_vars.scss @@ -0,0 +1,23 @@ +$primary: #1e2932; +$primaryForeground: #f0f0f0; +$secondary: #00766c; +$secondaryForeground: $primaryForeground; + +$backgroundColor: $primary; +$defaultTextColor: $primaryForeground; + +$info: #49fb; +$infoText: darken($info, 42%); +$infoColor: desaturate($infoText, 50%); + +$success: #5f5b; +$successText: darken($success, 45%); +$successColor: desaturate($successText, 50%); + +$warning: #fc0b; +$warningText: darken($warning, 30%); +$warningColor: desaturate($warningText, 50%); + +$error: #f00b; +$errorText: darken($error, 30%); +$errorColor: desaturate($errorText, 50%); diff --git a/assets/sass/app.scss b/assets/sass/app.scss new file mode 100644 index 0000000..933fea1 --- /dev/null +++ b/assets/sass/app.scss @@ -0,0 +1 @@ +@import "layout"; \ No newline at end of file diff --git a/assets/sass/error.scss b/assets/sass/error.scss new file mode 100644 index 0000000..d36ad0b --- /dev/null +++ b/assets/sass/error.scss @@ -0,0 +1,90 @@ +@import "layout"; + +header, footer { + margin: 0; + padding: 0; + height: 0; +} + +main { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .messages { + margin-bottom: 32px; + } + + .error-code { + font-size: 36px; + } + + .error-message { + font-size: 32px; + } + + .error-instructions { + margin-top: 32px; + font-size: 20px; + } + + nav { + margin-top: 32px; + } + + &::before { + content: "Oops"; + position: absolute; + z-index: -1; + + font-size: #{'min(50vh, 40vw)'}; + opacity: 0.025; + } +} + +.contact { + text-align: center; + padding: 8px; +} + +.logo { + position: absolute; + top: 0; + left: 0; + width: 100%; + margin-top: 24px; + text-align: center; + + a { + position: relative; + padding: 16px; + + color: $defaultTextColor; + + &:hover { + color: #fff; + + &::before { + opacity: 0.2; + } + } + + &::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + + background-image: url(../img/logo.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 64px; + + opacity: 0.075; + filter: contrast(0); + } + } +} \ No newline at end of file diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss new file mode 100644 index 0000000..0eb03a5 --- /dev/null +++ b/assets/sass/layout.scss @@ -0,0 +1,512 @@ +@import "vars"; +@import 'fonts'; + +* { + box-sizing: border-box; +} + +html, body { + height: 100%; +} + +body { + display: flex; + flex-direction: column; + + margin: 0; + font-family: "Nunito Sans", sans-serif; + font-size: 16px; + + color: $defaultTextColor; + background-color: $backgroundColor; +} + +header { + display: flex; + flex-direction: row; + justify-content: space-between; + + $headerHeight: 64px; + height: $headerHeight; + line-height: $headerHeight; + + $headerBackground: darken($primary, 1.75%); + background-color: $headerBackground; + + .logo { + display: flex; + flex-direction: row; + + padding: 0 24px 0 16px; + font-size: 32px; + color: $defaultTextColor; + + &:hover { + color: lighten($defaultTextColor, 10%); + } + + img { + width: $headerHeight; + height: $headerHeight; + margin-right: 16px; + } + } + + nav ul { + display: flex; + flex-direction: row; + margin: 0; + padding: 0; + + font-size: 20px; + + li { + list-style: none; + + a, span { + display: flex; + flex-direction: row; + align-items: center; + height: 64px; + padding: 0 24px; + + .feather { + --icon-size: 24px; + margin-right: 10px; + } + } + + a:hover { + background-color: #fff1; + } + } + } +} + +footer { + padding: 8px; + margin-top: 8px; + text-align: center; + background-color: darken($primary, 3%); +} + +main { + flex: 1; + padding: 8px; +} + +h1 { + text-align: center; + font-size: 32px; + + & + p { + text-align: center; + font-size: 20px; + } +} + +h1, h2 { + font-weight: 100; +} + +h3, h4 { + font-weight: 300; +} + +section > h2, .panel > h2 { + display: flex; + flex-direction: row; + align-items: center; + position: relative; + text-align: center; + margin-top: 16px; + + &::before, &::after { + content: ""; + flex: 1; + margin: 0 32px; + height: 0; + border-bottom: 1px solid $defaultTextColor; + opacity: 0.2; + } +} + +section > hr, .panel > hr { + border: 0; + border-bottom: 1px solid $defaultTextColor; + opacity: 0.2; + + margin: 8px 32px; +} + +a { + color: $secondary; + text-decoration: none; + + &:hover { + color: lighten($secondary, 10%); + } + + .feather.feather-external-link { + --icon-size: 16px; + margin-left: 4px; + margin-top: -3px; + } +} + +form { + padding: 8px 16px; + text-align: center; + + .form-field { + position: relative; + display: flex; + flex-direction: column; + margin: 16px auto; + + label { + position: absolute; + left: 8px; + top: 20px; + user-select: none; + font-size: 16px; + opacity: 0.75; + + transition-property: top, font-size; + transition-duration: 150ms; + transition-timing-function: ease-out; + + cursor: text; + } + + input, select, .input-group { + border: 0; + border-bottom: 2px solid #0008; + color: $defaultTextColor; + background: rgba(255, 255, 255, 0.05); + border-radius: 3px 3px 0 0; + font-size: 16px; + + &:focus, &:not([value=""]) { + ~ label { + top: 8px; + font-size: 14px; + } + } + } + + input, select, .form-display { + display: block; + padding: 32px 8px 8px 8px; + width: 100%; + } + + select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + &::-ms-expand { + display: none; + } + + & + .feather { + position: absolute; + z-index: -1; + right: 8px; + bottom: 8px; + + transition: transform 150ms ease-out; + } + + // Temporary + &:focus + .feather { + transform: rotateX(180deg); + } + } + + input[type=color] { + height: calc(32px + 8px + 32px); + } + + &.inline { + flex-direction: row; + + input[type=checkbox] { + text-align: left; + width: min-content; + height: min-content; + + & ~ label { + position: static; + display: inline; + padding-left: 8px; + font-size: 16px; + } + } + } + + .input-group { + display: flex; + flex-shrink: 1; + flex-direction: row; + + div { + position: relative; + flex: 1; + + input { + width: 100%; + margin-top: 24px; + padding-top: 8px; + border: 0; + background: transparent; + } + + > input + * { + position: absolute; + top: 32px; + right: 28px; + user-select: none; + text-align: right; + } + } + } + + .error, .hint { + padding: 2px; + text-align: left; + font-size: 14px; + + .feather { + --icon-size: 14px; + } + } + + .error { + color: $error; + } + } +} + +button, .button { + display: inline-flex; + margin: 8px; + padding: 12px 16px; + border: 0; + border-radius: 5px; + cursor: pointer; + + text-transform: uppercase; + font-size: 16px; + font-weight: bolder; + + line-height: 16px; + + .feather { + --icon-size: 16px; + margin-right: 8px; + } + + &, &.primary { + color: $primaryForeground; + background-color: $secondary; + + &:hover { + background-color: lighten($secondary, 10%); + } + } + + &.info { + background-color: $infoColor; + } + + &.success { + background-color: $successColor; + } + + &.warning { + background-color: $warningColor; + } + + &.error, &.danger { + background-color: $errorColor; + + &:hover { + background-color: lighten($errorColor, 10%); + } + } + + &.transparent { + background-color: transparent; + } + + &:hover { + color: $primaryForeground; + } +} + +.data-table { + width: 100%; + text-align: left; + border-collapse: collapse; + + th, td { + padding: 8px; + } + + th { + border-bottom: 1px solid #ffffff17; + } + + tr:nth-child(even) { + background-color: #ffffff08; + } + + tr:hover { + background-color: #ffffff18; + } + + thead tr:hover { + background-color: transparent; + } +} + +// --- +// --- Layout helpers +// --- +.center { + text-align: center; +} + +.container { + padding: 0 16px; + max-width: 632px; + margin: 0 auto; +} + +.panel { + margin: 16px 0; + padding: 8px; + border: 1px solid rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.03); + border-radius: 5px; + + p { + margin: 16px 8px; + } +} + +.sub-panel { + margin: 32px -18px; + padding: 1px 16px; + border: 2px solid rgba(255, 255, 255, 0.05); + border-radius: 5px; + + input, select, .input-group { + border-radius: 5px !important; + border-width: 0 !important; + } +} + + +// --- +// --- Feather +// --- +.feather { + flex-shrink: 0; + --icon-size: 24px; + width: var(--icon-size); + height: var(--icon-size); + stroke: currentColor; + stroke-width: 2; + stroke-linecap: square; + stroke-linejoin: miter; + fill: none; + vertical-align: middle; +} + +// --- +// --- Helper classes +// --- +.message { + display: flex; + flex-direction: row; + align-items: center; + + padding: 8px 16px; + + border-radius: 5px; + + .feather { + --icon-size: 24px; + margin-right: 8px; + } + + &:not(&-discreet) { + background-color: #fff5; + + &[data-type=info], &[data-type=question] { + background-color: $infoColor; + } + + &[data-type=success] { + background-color: $successColor; + } + + &[data-type=warning] { + background-color: $warningColor; + } + + &[data-type=error] { + background-color: $errorColor; + } + } + + &-discreet { + opacity: 0.75; + + .feather { + --icon-size: 20px; + } + } +} + +.messages .message:not(:last-child) { + margin-bottom: 8px; +} + +.container > .messages:first-child { + margin-top: 16px; +} + +.copyable-text { + display: flex; + flex-direction: row; + margin: 8px; + + background-color: darken($backgroundColor, 2%); + border-radius: 5px; + overflow: hidden; + + .title { + padding: 8px; + } + + .content { + overflow: hidden; + white-space: nowrap; + padding: 8px; + } + + .copy-button { + margin: 0; + padding: 0; + border-radius: 0; + + .feather { + --icon-size: 20px; + margin: 8px; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 9ce3150..d088e21 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,48 @@ "name": "aldap", "version": "0.1.0", "description": "Authentication LDAP server", - "main": "index.js", "repository": "git@gitlab.com:ArisuOngaku/aldap.git", "author": "Alice Gaudon ", - "license": "MIT", "private": true, + "main": "dist/main.js", + "scripts": { + "test": "jest --verbose --runInBand", + "dist-webpack": "webpack --mode production", + "dist": "tsc && npm run dist-webpack", + "dev": "concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon dist/main.js\" \"webpack --watch --mode development\" \"maildev\"", + "start": "yarn dist && node dist/main.js" + }, "devDependencies": { + "@babel/core": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "@types/config": "^0.0.36", + "@types/express": "^4.17.6", "@types/node": "^13.13.2", + "babel-loader": "^8.1.0", + "concurrently": "^5.1.0", + "css-loader": "^3.5.2", + "feather-icons": "^4.28.0", + "file-loader": "^6.0.0", + "imagemin": "^7.0.1", + "imagemin-gifsicle": "^7.0.0", + "imagemin-mozjpeg": "^8.0.0", + "imagemin-pngquant": "^8.0.0", + "imagemin-svgo": "^7.1.0", + "img-loader": "^3.0.1", "jest": "^25.4.0", + "mini-css-extract-plugin": "^0.9.0", + "node-sass": "^4.14.0", + "nodemon": "^2.0.3", + "sass-loader": "^8.0.2", "ts-jest": "^25.4.0", "typescript": "^3.8.3", + "uglifyjs-webpack-plugin": "^2.2.0", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.11", "wms-core": "^0.2.0" }, "dependencies": { - "config": "^3.3.1" + "config": "^3.3.1", + "express": "^4.17.1" } } diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..478541b --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,86 @@ +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); + +const dev = process.env.NODE_ENV === 'development'; + +const userConfig = require('./assets/config.json'); +for (const b in userConfig.bundles) { + if (userConfig.bundles.hasOwnProperty(b)) { + userConfig.bundles[b] = `./assets/${userConfig.bundles[b]}`; + } +} + +const config = { + entry: userConfig.bundles, + output: { + path: path.resolve(__dirname, 'public/js'), + filename: '[name].js' + }, + devtool: dev ? 'eval-source-map' : undefined, + module: { + rules: [ + { + test: /\.js$/i, + use: [ + { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + } + } + ] + }, + { + test: /\.s[ac]ss$/i, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + publicPath: '/', + } + }, + 'css-loader', + 'sass-loader', + ] + }, + { + test: /\.(woff2?|eot|ttf|otf)$/i, + use: 'file-loader?name=../fonts/[name].[ext]', + }, + { + test: /\.(png|jpe?g|gif|svg)$/i, + use: [ + 'file-loader?name=../img/[name].[ext]', + { + loader: 'img-loader', + options: { + enabled: !dev, + plugins: [ + require('imagemin-gifsicle')({}), + require('imagemin-mozjpeg')({}), + require('imagemin-pngquant')({}), + require('imagemin-svgo')({}), + ] + } + } + ] + } + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '../css/[name].css', + }), + ] +}; + +if (!dev) { + config.optimization = { + minimizer: [ + new UglifyJSPlugin(), + ] + }; +} + +module.exports = config; \ No newline at end of file