added remember me feature #53 (#98)

cleanup event subscriber
This commit is contained in:
Kevin Papst
2018-01-17 19:55:26 +01:00
committed by GitHub
parent 7df21f3f5a
commit 9a161b8fd9
26 changed files with 193 additions and 116 deletions

8
composer.lock generated
View File

@@ -52,12 +52,12 @@
"source": {
"type": "git",
"url": "https://github.com/kevinpapst/AdminThemeBundle.git",
"reference": "1aa9b1637b2f827fe79ca1edff26092840e30716"
"reference": "d31084eff7888d76f7976700f9ac878f40c3bacd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/1aa9b1637b2f827fe79ca1edff26092840e30716",
"reference": "1aa9b1637b2f827fe79ca1edff26092840e30716",
"url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/d31084eff7888d76f7976700f9ac878f40c3bacd",
"reference": "d31084eff7888d76f7976700f9ac878f40c3bacd",
"shasum": ""
},
"require": {
@@ -111,7 +111,7 @@
"support": {
"source": "https://github.com/kevinpapst/AdminThemeBundle/tree/kevinpapst"
},
"time": "2018-01-15T19:14:46+00:00"
"time": "2018-01-17T12:31:37+00:00"
},
{
"name": "beberlei/DoctrineExtensions",

View File

@@ -24,6 +24,11 @@ security:
# denying access is done in access_control or in your controllers
anonymous: ~
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
# This allows the user to login by submitting a username and password
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
form_login:

View File

@@ -1,7 +1,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: 'en'
locale: en
app_locales: en|de
services:
@@ -33,19 +33,12 @@ services:
# ================================================================================
# a route listener, that injects the locale through a URL directory
app.redirect_to_preferred_locale_listener:
class: App\EventListener\RedirectToPreferredLocaleListener
App\EventSubscriber\RedirectToLocaleSubscriber:
class: App\EventSubscriber\RedirectToLocaleSubscriber
arguments: ['@router', '%app_locales%', '%locale%']
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
# additional menu entries for the timesheet bundle
timesheet.configure_menu_listener:
class: App\EventListener\Menu
tags:
- { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure }
- { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure }
# ================================================================================
# DATABASE
# ================================================================================
@@ -80,17 +73,6 @@ services:
# // same code using repository services
# $posts = $this->get('app.user_repository')->findAll();
# ================================================================================
# EVENTS and LISTENER / SUBSCRIBER
# ================================================================================
# event-listener to populate the navigation
app.menu_builder:
class: App\EventListener\MenuBuilder
arguments: ["@event_dispatcher", "@security.authorization_checker"]
tags:
- { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupNavbar }
# ================================================================================
# FORMS
# ================================================================================
@@ -132,13 +114,6 @@ services:
App\Twig\Extensions:
arguments: ['%app_locales%']
avanzu_admin_theme.navbar_user_listener:
class: App\EventListener\NavbarShowUserListener
arguments: ["@security.token_storage"]
tags:
- { name: kernel.event_listener, event: theme.navbar_user, method: onShowUser }
- { name: kernel.event_listener, event: theme.sidebar_user, method: onShowUser }
# avanzu_admin_theme.navbar_task_listener:
# class: "%avanzu_admin_theme.navbar_task_listener.class%"
# tags:

View File

@@ -79,6 +79,11 @@ footer.main-footer {
font-size: 80%;
}
/* ================================ LOGIN-PAGE ================================ */
.login-page form label {
padding-left: 5px;
}
/* ================================ DATA-TABLES ================================ */

View File

@@ -30,6 +30,7 @@ use App\Repository\Query\ActivityQuery;
*
* @Route("/admin/activity")
* @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/

View File

@@ -28,6 +28,7 @@ use App\Repository\Query\CustomerQuery;
*
* @Route("/admin/customer")
* @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/

View File

@@ -29,6 +29,7 @@ use App\Repository\Query\ProjectQuery;
*
* @Route("/admin/project")
* @Security("has_role('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/

View File

@@ -20,7 +20,6 @@ use App\Entity\Timesheet;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use App\Form\TimesheetAdminForm;
/**
@@ -28,6 +27,7 @@ use App\Form\TimesheetAdminForm;
*
* @Route("/team/timesheet")
* @Security("has_role('ROLE_TEAMLEAD')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
@@ -41,7 +41,6 @@ class TimesheetController extends AbstractController
* @Route("/", defaults={"page": 1}, name="admin_timesheet")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated")
* @Method("GET")
* @Cache(smaxage="10")
*
* @param $page
* @param Request $request

View File

@@ -28,6 +28,7 @@ use Symfony\Component\HttpFoundation\Request;
*
* @Route("/admin/user")
* @Security("has_role('ROLE_SUPER_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
@@ -61,7 +62,6 @@ class UserController extends AbstractController
* @Route("/", defaults={"page": 1}, name="admin_user")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
* @Method("GET")
* @Security("is_granted('view_all', user)")
*/
public function indexAction($page, Request $request)
{

View File

@@ -31,23 +31,16 @@ abstract class ConfigureMenuEvent extends Event
* @var SidebarMenuEvent
*/
private $event;
/**
* @var AuthorizationChecker
*/
private $auth;
/**
* ConfigureMenuEvent constructor.
* @param AuthorizationChecker $auth
* @param Request $request
* @param SidebarMenuEvent $event
*/
public function __construct(
AuthorizationChecker $auth,
Request $request,
SidebarMenuEvent $event
) {
$this->auth = $auth;
$this->request = $request;
$this->event = $event;
}
@@ -67,12 +60,4 @@ abstract class ConfigureMenuEvent extends Event
{
return $this->event;
}
/**
* @return AuthorizationChecker
*/
public function getAuth()
{
return $this->auth;
}
}

View File

@@ -9,42 +9,53 @@
* file that was distributed with this source code.
*/
namespace App\EventListener;
namespace App\EventSubscriber;
use App\Event\ConfigureMainMenuEvent;
use App\Event\ConfigureAdminMenuEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Avanzu\AdminThemeBundle\Model\MenuItemModel;
use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Class MenuBuilder configures the main navigation.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class MenuBuilder
class MenuBuilderSubscriber implements EventSubscriberInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var AuthorizationChecker
* @var AuthorizationCheckerInterface
*/
private $security;
/**
* MenuBuilder constructor.
* MenuBuilderSubscriber constructor.
* @param EventDispatcherInterface $dispatcher
* @param AuthorizationChecker $security
* @param AuthorizationCheckerInterface $security
*/
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationChecker $security)
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $security)
{
$this->eventDispatcher = $dispatcher;
$this->security = $security;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'theme.sidebar_setup_menu' => ['onSetupNavbar', 100],
];
}
/**
* Generate the main menu.
*
@@ -53,9 +64,8 @@ class MenuBuilder
public function onSetupNavbar(SidebarMenuEvent $event)
{
$request = $event->getRequest();
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_FULLY');
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_REMEMBERED');
$isTeamlead = $isLoggedIn && $this->security->isGranted('ROLE_TEAMLEAD');
$isSuperAdmin = $isLoggedIn && $this->security->isGranted('ROLE_SUPER_ADMIN');
$event->addItem(
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fa fa-dashboard')
@@ -64,7 +74,6 @@ class MenuBuilder
$this->eventDispatcher->dispatch(
ConfigureMainMenuEvent::CONFIGURE,
new ConfigureMainMenuEvent(
$this->security,
$request,
$event
)
@@ -74,16 +83,9 @@ class MenuBuilder
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fa fa-wrench');
$event->addItem($admin);
if ($isSuperAdmin) {
$admin->addChild(
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fa fa-user')
);
}
$this->eventDispatcher->dispatch(
ConfigureAdminMenuEvent::CONFIGURE,
new ConfigureAdminMenuEvent(
$this->security,
$request,
$event
)

View File

@@ -9,29 +9,55 @@
* file that was distributed with this source code.
*/
namespace App\EventListener;
namespace App\EventSubscriber;
use App\Event\ConfigureMainMenuEvent;
use App\Event\ConfigureAdminMenuEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Avanzu\AdminThemeBundle\Model\MenuItemModel;
use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Menus for timesheet
* Menu event subscriber for timesheet, customer, projects, activities.
* This is a sample implementation for developer who want to add new navigation entries in their bundles.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class Menu
class MenuSubscriber implements EventSubscriberInterface
{
/**
* @var AuthorizationCheckerInterface
*/
private $security;
/**
* MenuSubscriber constructor.
* @param AuthorizationCheckerInterface $security
*/
public function __construct(AuthorizationCheckerInterface $security)
{
$this->security = $security;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
ConfigureMainMenuEvent::CONFIGURE => ['onMainMenuConfigure', 100],
ConfigureAdminMenuEvent::CONFIGURE => ['onAdminMenuConfigure', 100],
];
}
/**
* @param \App\Event\ConfigureMainMenuEvent $event
*/
public function onMainMenuConfigure(ConfigureMainMenuEvent $event)
{
$auth = $event->getAuth();
$auth = $this->security;
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_FULLY');
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_REMEMBERED');
$isUser = $isLoggedIn && $auth->isGranted('ROLE_USER');
if (!$isLoggedIn || !$isUser) {
@@ -50,13 +76,9 @@ class Menu
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
{
$menu = $event->getAdminMenu();
$auth = $event->getAuth();
$auth = $this->security;
if (!$auth->isGranted('IS_AUTHENTICATED_FULLY')) {
return;
}
if (!$auth->isGranted('ROLE_TEAMLEAD')) {
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED') || !$auth->isGranted('ROLE_TEAMLEAD')) {
return;
}
@@ -68,13 +90,18 @@ class Menu
return;
}
if ($auth->isGranted('ROLE_SUPER_ADMIN')) {
$menu->addChild(
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fa fa-user')
);
}
$menu->addChild(
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fa fa-users')
)->addChild(
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fa fa-book')
)->addChild(
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fa fa-tasks')
)
;
);
}
}

View File

@@ -9,19 +9,20 @@
* file that was distributed with this source code.
*/
namespace App\EventListener;
namespace App\EventSubscriber;
use App\Entity\User;
use Avanzu\AdminThemeBundle\Event\ShowUserEvent;
use Avanzu\AdminThemeBundle\Model\UserModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Class NavbarShowUserListener
* Class NavbarShowUserSubscriber
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class NavbarShowUserListener
class NavbarShowUserSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
@@ -37,6 +38,17 @@ class NavbarShowUserListener
$this->storage = $tokenStorage;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'theme.navbar_user' => ['onShowUser', 100],
'theme.sidebar_user' => ['onShowUser', 100],
];
}
/**
* @param ShowUserEvent $event
*/

View File

@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace App\EventListener;
namespace App\EventSubscriber;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -23,7 +23,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class RedirectToPreferredLocaleListener
class RedirectToLocaleSubscriber
{
/**
* @var UrlGeneratorInterface

View File

@@ -36,6 +36,19 @@ abstract class AbstractVoter extends Voter
$this->decisionManager = $decisionManager;
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function isFullyAuthenticated(TokenInterface $token)
{
if ($this->decisionManager->decide($token, ['IS_AUTHENTICATED_FULLY'])) {
return true;
}
return false;
}
/**
* @param string $role
* @param TokenInterface $token
@@ -43,7 +56,7 @@ abstract class AbstractVoter extends Voter
*/
protected function hasRole($role, TokenInterface $token)
{
if ($this->decisionManager->decide($token, array($role))) {
if ($this->decisionManager->decide($token, [$role])) {
return true;
}

View File

@@ -27,6 +27,12 @@ class ActivityVoter extends AbstractVoter
const EDIT = 'edit';
const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/**
* @param string $attribute
* @param mixed $subject
@@ -34,7 +40,7 @@ class ActivityVoter extends AbstractVoter
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -107,6 +113,6 @@ class ActivityVoter extends AbstractVoter
*/
protected function canDelete(TokenInterface $token)
{
return $this->hasRole('ROLE_ADMIN', $token);
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -27,6 +27,12 @@ class CustomerVoter extends AbstractVoter
const EDIT = 'edit';
const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/**
* @param string $attribute
* @param mixed $subject
@@ -34,7 +40,7 @@ class CustomerVoter extends AbstractVoter
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -107,6 +113,6 @@ class CustomerVoter extends AbstractVoter
*/
protected function canDelete(TokenInterface $token)
{
return $this->hasRole('ROLE_ADMIN', $token);
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -27,6 +27,12 @@ class ProjectVoter extends AbstractVoter
const EDIT = 'edit';
const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/**
* @param string $attribute
* @param mixed $subject
@@ -34,7 +40,7 @@ class ProjectVoter extends AbstractVoter
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -107,6 +113,6 @@ class ProjectVoter extends AbstractVoter
*/
protected function canDelete(TokenInterface $token)
{
return $this->hasRole('ROLE_ADMIN', $token);
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -30,6 +30,14 @@ class TimesheetVoter extends AbstractVoter
const EDIT = 'edit';
const DELETE = 'delete';
const ALLOWED_ATTRIBUTES = [
self::START,
self::STOP,
self::VIEW,
self::EDIT,
self::DELETE
];
/**
* @param string $attribute
* @param mixed $subject
@@ -37,7 +45,7 @@ class TimesheetVoter extends AbstractVoter
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::START, self::STOP, self::VIEW, self::EDIT, self::DELETE))) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -146,6 +154,10 @@ class TimesheetVoter extends AbstractVoter
*/
protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token)
{
if (!$this->isFullyAuthenticated($token)) {
return false;
}
return $this->isOwnOrAdmin($timesheet, $user, $token);
}

View File

@@ -28,11 +28,9 @@ class UserVoter extends AbstractVoter
const PASSWORD = 'password';
const ROLES = 'roles';
const PREFERENCES = 'preferences';
const VIEW_ALL = 'view_all';
const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::VIEW_ALL,
self::EDIT,
self::CREATE,
self::ROLES,
@@ -82,7 +80,6 @@ class UserVoter extends AbstractVoter
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($subject, $user, $token);
case self::VIEW_ALL:
case self::CREATE: // create actually passes in the current user as $subject, not the new one
case self::ROLES:
return $this->canAdminUsers($token);
@@ -135,6 +132,6 @@ class UserVoter extends AbstractVoter
*/
protected function canAdminUsers(TokenInterface $token)
{
return $this->hasRole('ROLE_SUPER_ADMIN', $token);
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_SUPER_ADMIN', $token);
}
}

View File

@@ -48,13 +48,13 @@
{% endblock %}
{% block avanzu_navbar_messages %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ render(controller('App\\Controller\\TimesheetController::activeEntriesAction')) }}
{% endif %}
{% endblock %}
{% block avanzu_navbar_notifications %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_FULLY') %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ render(controller('App\\Controller\\ActivityController::recentActivitiesAction')) }}
{% endif %}
{% endblock %}

View File

@@ -2,6 +2,10 @@
{% block avanzu_login_title %}{{ 'browser.title'|trans }}{% endblock %}
{% block avanzu_logo_login %}<b>Kimai</b><br>TimeTracking{% endblock %}
{% block avanzu_head %}
<link rel="stylesheet" href="{{ asset('css/kimai.css') }}">
{% endblock %}
{% block avanzu_login_box_msg %}
{{ 'security.title.login'|trans }}
@@ -21,13 +25,12 @@
</div>
<div class="row">
<div class="col-xs-8">
{#
<div class="checkbox icheck">
<label>
<input type="checkbox"> {{ 'Remember Me'|trans({}, 'AvanzuAdminTheme') }}
</label>
</div>
#}
<div class="checkbox icheck">
<label>
<input id="remember_me" name="_remember_me" type="checkbox">
<label for="remember_me">{{ 'Remember Me'|trans({}, 'AvanzuAdminTheme') }}</label>
</label>
</div>
</div>
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">{{ 'action.sign_in'|trans }}</button>

View File

@@ -38,10 +38,7 @@
<td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td>
<td class="hidden-xs hidden-sm">{{ entry.description }}</td>
<td>
{% set actionButtons = {} %}
{% if is_granted('edit', entry) %}
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})}|merge(actionButtons) %}
{% endif %}
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
{% if entry.end %}
{% if is_granted('start', entry.activity) %}
{% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.activity.id})}|merge(actionButtons) %}
@@ -51,9 +48,7 @@
{% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
{% endif %}
{% endif %}
{% if is_granted('delete', entry) %}
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
{% endif %}
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
{{ widgets.button_group(actionButtons) }}
</td>
</tr>

View File

@@ -6,6 +6,10 @@
<source>Toggle navigation</source>
<target>Ansicht der Seitenleiste umschalten</target>
</trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Dauerhaft anmelden</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -6,6 +6,10 @@
<source>Toggle navigation</source>
<target>Toggle navigation</target>
</trans-unit>
<trans-unit id="Remember Me">
<source>Remember Me</source>
<target>Remember Me</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2,7 +2,7 @@
There are multiple pre-defined roles in Kimai, which define the ACLs. A user can only inherit one role, where the roles extend each user.
## Roles
## Roles & Permissions
| Role name | extends | Gives permission for |
|---|---|---|
@@ -12,3 +12,21 @@ There are multiple pre-defined roles in Kimai, which define the ACLs. A user can
| ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities |
| ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users |
# Login & Authentication
- User can login with their username or email
- If you activate the `Remember me` option, you can use use the most common functions within the next days without a new login
## Remember me login
If you have chosen to login with the `Remember me` option, your login will be extended to one week (default value).
After coming back and being remembered you have access to all the following features:
- view your own timesheet
- start and stop new records
- edit existing records
If you are an administrator, you will see all your allowed options in the menu, but will be redirected to the login
form when you try to access them. This is a security feature to prevent abuse in case you forgot to logout in public
environments.
The default period for the `Remember me` option can be changed in the config file [security.yaml](config/packages/security.yaml).