Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/src/.gitignore b/src/.gitignore
index 29988b9e..e7357141 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -1,23 +1,24 @@
*.swp
database/database.sqlite
node_modules/
package-lock.json
public/css/*.css
public/hot
public/js/*.js
public/storage/
storage/*.key
storage/export/
tests/report/
vendor
.env
.env.backup
.env.local
.env.testing
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
composer.lock
resources/countries.php
+resources/js/ts.js
diff --git a/src/resources/build/before.php b/src/resources/build/before.php
new file mode 100644
index 00000000..59a95cc4
--- /dev/null
+++ b/src/resources/build/before.php
@@ -0,0 +1,9 @@
+<?php
+
+$rootDir = __DIR__ . '/../..';
+
+// Write build timestamp to a file that is then included by the vue components
+file_put_contents(
+ "{$rootDir}/resources/js/ts.js",
+ sprintf("export default new Date('%s')", date('c'))
+);
diff --git a/src/resources/vue/Widgets/Menu.vue b/src/resources/vue/Widgets/Menu.vue
index 8ebd6285..a9ba4f01 100644
--- a/src/resources/vue/Widgets/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -1,99 +1,106 @@
<template>
<nav :id="mode + '-menu'" class="navbar navbar-expand-lg navbar-light">
<div class="container">
<router-link class="navbar-brand" to="/" v-html="$root.logo(mode)"></router-link>
<button v-if="mode == 'header'" class="navbar-toggler" type="button"
data-toggle="collapse" :data-target="'#' + mode + '-menu-navbar'"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div :id="mode + '-menu-navbar'" :class="'navbar' + (mode == 'header' ? ' collapse navbar-collapse' : '')">
<ul class="navbar-nav">
<li class="nav-item" v-for="item in menu()" :key="item.index">
<a v-if="item.href" :class="'nav-link link-' + item.index" :href="item.href">{{ item.title }}</a>
<router-link v-if="item.to"
:class="'nav-link link-' + item.index"
active-class="active"
:to="item.to"
:exact="item.exact"
>
{{ item.title }}
</router-link>
</li>
<li class="nav-item" v-if="!loggedIn && !$root.isAdmin">
<router-link class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
</li>
<li class="nav-item" v-if="loggedIn">
<router-link class="nav-link link-dashboard" active-class="active" :to="{name: 'dashboard'}">Cockpit</router-link>
</li>
<li class="nav-item" v-if="loggedIn">
<router-link class="nav-link menulogin link-logout" active-class="active" :to="{name: 'logout'}">Logout</router-link>
</li>
<li class="nav-item" v-if="!loggedIn">
<router-link class="nav-link menulogin link-login" :to="{name: 'login'}">Login</router-link>
</li>
</ul>
<div v-if="mode == 'footer'" class="footer">
- <div id="footer-copyright">@ Apheleia IT AG, 2020</div>
+ <div id="footer-copyright">@ Apheleia IT AG, {{ buildYear }}</div>
<div v-if="footer" id="footer-company">{{ footer }}</div>
</div>
</div>
</div>
</nav>
</template>
<script>
+ import buildDate from '../../js/ts.js'
+
export default {
+ data() {
+ return {
+ buildYear: buildDate.getFullYear()
+ }
+ },
props: {
mode: { type: String, default: 'header' },
footer: { type: String, default: '' }
},
computed: {
loggedIn() { return this.$store.state.isLoggedIn },
route() { return this.$route.name }
},
mounted() {
// On mobile close the menu when the menu item is clicked
if (this.mode == 'header') {
$('#header-menu .navbar').on('click', function() { $(this).removeClass('show') })
}
},
methods: {
menu() {
let menu = []
const loggedIn = this.loggedIn
window.config.menu.forEach(item => {
if (!item.location || !item.title) {
console.error("Invalid menu entry", item)
return
}
// TODO: Different menu for different loggedIn state
if (window.isAdmin && !item.admin) {
return
} else if (!window.isAdmin && item.admin === 'only') {
return
}
if (!item.footer || this.mode == 'footer') {
if (item.location.match(/^https?:/)) {
item.href = item.location
} else {
item.to = { path: item.location }
}
item.exact = item.location == '/'
item.index = item.page || item.title.toLowerCase().replace(/\s+/g, '')
menu.push(item)
}
})
return menu
}
}
}
</script>
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
index b1cae78a..def8c463 100644
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -1,238 +1,239 @@
<?php
namespace Tests\Browser;
use Tests\Browser;
use Tests\Browser\Components\Menu;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\UserProfile;
use Tests\TestCaseDusk;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class LogonTest extends TestCaseDusk
{
/**
* Test menu on logon page
*/
public function testLogonMenu(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'])
+ ->assertSeeIn('#footer-copyright', '@ Apheleia IT AG, ' . date('Y'));
});
if ($browser->isDesktop()) {
$browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
});
} else {
$browser->assertMissing('#footer-menu .navbar-nav');
}
$browser->assertSeeLink('Forgot password?')
->assertSeeLink('Webmail');
});
}
/**
* Test redirect to /login if user is unauthenticated
*/
public function testRequiredAuth(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard');
// Checks if we're really on the login page
$browser->waitForLocation('/login')
->on(new Home());
});
}
/**
* Logon with wrong password/user test
*/
public function testLogonWrongCredentials(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'wrong');
// Error message
$browser->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.');
// Checks if we're still on the logon page
$browser->on(new Home());
});
}
/**
* Successful logon test
*/
public function testLogonSuccessful(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
// Checks if we're really on Dashboard page
->on(new Dashboard())
->assertVisible('@links a.link-profile')
->assertVisible('@links a.link-domains')
->assertVisible('@links a.link-users')
->assertVisible('@links a.link-wallet')
->assertVisible('@links a.link-webmail')
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout']);
});
if ($browser->isDesktop()) {
$browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'dashboard', 'logout']);
});
} else {
$browser->assertMissing('#footer-menu .navbar-nav');
}
$browser->assertUser('john@kolab.org');
// Assert no "Account status" for this account
$browser->assertMissing('@status');
// Goto /domains and assert that the link on logo element
// leads to the dashboard
$browser->visit('/domains')
->waitForText('Domains')
->click('a.navbar-brand')
->on(new Dashboard());
// Test that visiting '/' with logged in user does not open logon form
// but "redirects" to the dashboard
$browser->visit('/')
->waitForLocation('/dashboard')
->on(new Dashboard());
});
}
/**
* Logout test
*
* @depends testLogonSuccessful
*/
public function testLogout(): void
{
$this->browse(function (Browser $browser) {
$browser->on(new Dashboard());
// Click the Logout button
$browser->within(new Menu(), function ($browser) {
$browser->clickMenuItem('logout');
});
// We expect the logon page
$browser->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
/**
* Logout by URL test
*/
public function testLogoutByURL(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard());
// Use /logout url, and expect the logon page
$browser->visit('/logout')
->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
/**
* Test 2-Factor Authentication
*
* @depends testLogoutByURL
*/
public function test2FA(): void
{
$this->browse(function (Browser $browser) {
// Test missing 2fa code
$browser->on(new Home())
->type('@email-input', 'ned@kolab.org')
->type('@password-input', 'simple123')
->press('form button')
->waitFor('@second-factor-input.is-invalid + .invalid-feedback')
->assertSeeIn(
'@second-factor-input.is-invalid + .invalid-feedback',
'Second factor code is required.'
)
->assertFocused('@second-factor-input')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
// Test invalid code
$browser->type('@second-factor-input', '123456')
->press('form button')
->waitUntilMissing('@second-factor-input.is-invalid')
->waitFor('@second-factor-input.is-invalid + .invalid-feedback')
->assertSeeIn(
'@second-factor-input.is-invalid + .invalid-feedback',
'Second factor code is invalid.'
)
->assertFocused('@second-factor-input')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
$code = \App\Auth\SecondFactor::code('ned@kolab.org');
// Test valid (TOTP) code
$browser->type('@second-factor-input', $code)
->press('form button')
->waitUntilMissing('@second-factor-input.is-invalid')
->waitForLocation('/dashboard')
->on(new Dashboard());
});
}
/**
* Test redirect to the requested page after logon
*
* @depends test2FA
*/
public function testAfterLogonRedirect(): void
{
$this->browse(function (Browser $browser) {
// User is logged in
$browser->visit(new UserProfile());
// Test redirect if the token is invalid
$browser->script("localStorage.setItem('token', '123')");
$browser->refresh()
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', false)
->waitForLocation('/profile');
});
}
}
diff --git a/src/webpack.mix.js b/src/webpack.mix.js
index 5599af72..056e0f3e 100644
--- a/src/webpack.mix.js
+++ b/src/webpack.mix.js
@@ -1,39 +1,44 @@
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
+const { exec } = require('child_process');
const fs = require('fs');
const glob = require('glob');
const mix = require('laravel-mix');
mix.webpackConfig({
resolve: {
alias: {
'jquery$': 'jquery/dist/jquery.slim.js',
}
}
})
+mix.before(() => {
+ exec('php resources/build/before.php')
+})
+
mix.js('resources/js/user.js', 'public/js').vue()
.js('resources/js/admin.js', 'public/js').vue()
glob.sync('resources/themes/*/', {}).forEach(fromDir => {
const toDir = fromDir.replace('resources/themes/', 'public/themes/')
mix.sass(fromDir + 'app.scss', toDir)
.sass(fromDir + 'document.scss', toDir);
fs.stat(fromDir + 'images', {}, (err, stats) => {
if (stats) {
mix.copyDirectory(fromDir + 'images', toDir + 'images')
}
})
})

File Metadata

Mime Type
text/x-diff
Expires
Mon, Feb 2, 7:40 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426993
Default Alt Text
(15 KB)

Event Timeline