Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2529639
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Feb 2, 7:40 PM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426993
Default Alt Text
(15 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment