Commit 9dfce47d authored by Christophe Maudoux's avatar Christophe Maudoux 🐛

WIP - Append U2F module to manage users U2F Key (delete only at the moment)

parent 31398c78
node_modules
e2e-tests/conf/
lemonldap-ng-common/MYMETA.json
lemonldap-ng-common/MYMETA.yml
lemonldap-ng-common/Makefile
lemonldap-ng-common/blib/
lemonldap-ng-common/pm_to_blib
lemonldap-ng-handler/MYMETA.json
lemonldap-ng-handler/MYMETA.yml
lemonldap-ng-handler/Makefile
lemonldap-ng-handler/blib/
lemonldap-ng-handler/pm_to_blib
lemonldap-ng-manager/MYMETA.json
lemonldap-ng-manager/MYMETA.yml
lemonldap-ng-manager/Makefile
lemonldap-ng-manager/blib/
lemonldap-ng-manager/pm_to_blib
lemonldap-ng-portal/MYMETA.json
lemonldap-ng-portal/MYMETA.yml
lemonldap-ng-portal/Makefile
lemonldap-ng-portal/blib/
lemonldap-ng-portal/t/
.gitignore
lemonldap-ng-portal/pm_to_blib
e2e-tests/lemonldap-ng.ini
......@@ -32,6 +32,7 @@ useRedirectOnError = 0
[manager]
enabledModules = conf, sessions, notifications, U2F
protection = manager
staticPrefix = /static
languages = fr, en, vi, ar
......
......@@ -33,6 +33,8 @@ sub hAttr {
$_[0]->{hiddenAttributes} || $_[0]->conf->{hiddenAttributes};
}
### SEE LEMONLDAP::NG::COMMON::SESSION FOR AVAILABLE FUNCTIONS
sub delSession {
my ( $self, $req ) = @_;
return $self->sendJSONresponse( $req, { result => 1 } )
......@@ -61,20 +63,17 @@ sub delU2FKey {
# Try to read session
my $apacheSession = $self->getApacheSession( $mod, $id )
my $session = $self->getApacheSession( $mod, $id )
or return $self->sendError( $req, undef, 400 );
my %session = %{ $apacheSession->data };
$session{_session_uid} = 'TOTO';
$apacheSession->update(\%session);
#return $self->sendError( $req, $session->data->{_session_uid}, 666 );
# Delete U2F key attributs and update session
$session->data->{_u2fKeyHandle} = 'TOF';
$session->data->{_u2fUserKey} = 'TOF';
$session->update( \%{$session->data} );
Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id );
if ( $apacheSession->error ) {
return $self->sendError( $req, $apacheSession->error, 200 );
if ( $session->error ) {
return $self->sendError( $req, $session->error, 200 );
}
return $self->sendJSONresponse( $req, { result => 1 } );
}
......
......@@ -90,7 +90,7 @@ sub init {
$self->defaultRoute( $working[0]->defaultRoute );
my $linksIcons =
{ 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell' };
{ 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell', 'U2F' => 'wrench' };
$self->links( [] );
for ( my $i = 0 ; $i < @links ; $i++ ) {
......
......@@ -31,19 +31,19 @@ sub addRoutes {
$self->addRoute( 'u2f.html', undef, ['GET'] )
# READ
->addRoute( sessions => { ':sessionType' => 'sessions' }, ['GET'] )
->addRoute( u2f => { ':sessionType' => 'sessions' }, ['GET'] )
# DELETEU2FKey
# DELETE U2F KEY ATTRIBUTS
->addRoute(
sessions => { ':sessionType' => { ':sessionId' => 'delU2FKey' } },
['POST']
u2f => { ':sessionType' => { ':sessionId' => 'delU2FKey' } },
['DELETE']
);
# DELETE
#~ ->addRoute(
#~ sessions => { ':sessionType' => { ':sessionId' => 'delSession' } },
#~ ['DELETE']
#);
# UPDATE U2F KEY ATTRIBUTS
# ->addRoute(
# u2f => { ':sessionType' => { ':sessionId' => { ':updateSession' } },
# ['PUT']
#);
$self->setTypes($conf);
......@@ -56,7 +56,7 @@ sub addRoutes {
# II. DISPLAY METHODS #
#######################
sub sessions {
sub u2f {
my ( $self, $req, $session, $skey ) = @_;
# Case 1: only one session is required
......
###
# U2F manager
# Session explorer
###
# Max number of session to display (see overScheme)
......@@ -119,7 +119,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
$scope.translate = $translator.translate
$scope.translateTitle = (node) ->
$translator.translateField node, 'title'
sessionType = 'Persistent'
sessionType = 'global'
# Handle menu items
$scope.menuClick = (button) ->
......@@ -138,10 +138,10 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
# SESSION MANAGEMENT
# Delete U2F Key
# Delete
$scope.deleteU2FKey = ->
$scope.waiting = true
$http['post']("#{scriptname}sessions/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$http['delete']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
$scope.currentScope.remove()
$scope.waiting = false
......
// Generated by CoffeeScript 1.10.0
// Generated by CoffeeScript 1.9.3
/*
* Session explorer
......
// Generated by CoffeeScript 1.9.3
/*
* U2F manager
* Session explorer
*/
(function() {
......@@ -141,7 +141,7 @@
$scope.translateTitle = function(node) {
return $translator.translateField(node, 'title');
};
sessionType = 'Persistent';
sessionType = 'global';
$scope.menuClick = function(button) {
if (button.popup) {
window.open(button.popup);
......@@ -164,7 +164,7 @@
};
$scope.deleteU2FKey = function() {
$scope.waiting = true;
return $http['post'](scriptname + "sessions/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
return $http['delete'](scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
$scope.currentSession = null;
$scope.currentScope.remove();
return $scope.waiting = false;
......
(function(){var c,e,d,b,g,f,a;b=25;a={_whatToTrace:[function(i,h){return"groupBy=substr("+i+",1)"},function(i,h){return i+"="+h+"*&groupBy="+i},function(i,h){return i+"="+h}],ipAddr:[function(i,h){return"groupBy=net("+i+",16,1)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",32,2)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",48,3)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",128,4)"},function(i,h){return i+"="+h+"&groupBy=_whatToTrace"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],_startTime:[function(i,h){return"groupBy=substr("+i+",8)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",10)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",11)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",12)"},function(i,h){return i+"="+h+"*&groupBy=_whatToTrace"},function(i,h,j){console.log(i);console.log(h);console.log(j);return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],doubleIp:[function(i,h){return i},function(i,h){return"_whatToTrace="+h+"&groupBy=ipAddr"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&ipAddr="+h)}]};f={_whatToTrace:function(i,h,k,j){if(k===1){return i+"="+h+"*&groupBy=substr("+i+","+(k+j+1)+")"}else{return null}},ipAddr:function(i,h,k,j){if(k>0&&k<4){return i+"="+h+"*&groupBy=net("+i+","+(16*k+4*(j+1))+",2)"}else{return null}}};e="_password";c={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],BrowserID:["_browserIdAnswer","_browserIdAnswerRaw"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token"]};g={session:[{title:"deleteU2FKey",icon:"trash"}],home:[]};d=angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]);d.controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(p,h,i,j,m){var n,l,k,o;p.links=links;p.menulinks=menulinks;p.staticPrefix=staticPrefix;p.scriptname=scriptname;p.formPrefix=formPrefix;p.availableLanguages=availableLanguages;p.waiting=true;p.showM=false;p.showT=true;p.data=[];p.currentScope=null;p.currentSession=null;p.menu=g;p.translateP=h.translateP;p.translate=h.translate;p.translateTitle=function(q){return h.translateField(q,"title")};o="Persistent";p.menuClick=function(q){if(q.popup){window.open(q.popup)}else{if(!q.action){q.action=q.title}switch(typeof q.action){case"function":q.action(p.currentNode,p);break;case"string":p[q.action]();break;default:console.log(typeof q.action)}}return p.showM=false};p.deleteU2FKey=function(){p.waiting=true;return m.post(scriptname+"sessions/"+o+"/"+p.currentSession.id).then(function(q){p.currentSession=null;p.currentScope.remove();return p.waiting=false},function(q){p.currentSession=null;p.currentScope.remove();return p.waiting=false})};p.stoggle=function(q){var r;r=q.$modelValue;if(r.nodes.length===0){p.updateTree(r.value,r.nodes,r.level,r.over,r.query,r.count)}return q.toggle()};p.displaySession=function(r){var s,q;q=function(t){var y,A,E,C,G,J,B,I,H,O,F,K,x,w,u,z,N,M,v,L,D;A=function(P){return P};y=function(S,U){var Q,R,P,T;P=[];R=new RegExp(S);for(Q in t){T=t[Q];if(Q.match(R)&&T){P.push({title:Q,value:T});delete t[Q]}}if(P.length>0){return N.push({title:U,nodes:P})}};v=t._utime;B=t._session_id;for(O in t){D=t[O];if(!D){delete t[O]}else{if(typeof t==="string"&&D.match(/; /)){t[O]=D.split("; ")}if(typeof t[O]!=="object"){if(e.match(new RegExp("\b"+O+"\b"))){t[O]="********"}else{if(O.match(/^(_utime|_lastAuthnUTime|_lastSeen|notification)$/)){t[O]=p.localeDate(D)}else{if(O.match(/^(_startTime|_updateTime)$/)){t[O]=A(D)}}}}}}N=[];for(G in c){C=c[G];M=[];for(J=0,K=C.length;J<K;J++){E=C[J];if(t[E]){M.push({title:E,value:t[E]});delete t[E]}}if(M.length>0){N.push({title:"__"+G+"__",nodes:M})}}y("^openid","OpenID");y("^notification_(.+)","__notificationsDone__");if(t._loginHistory){L=[];if(t._loginHistory.successLogin){u=t._loginHistory.successLogin;for(I=0,x=u.length;I<x;I++){F=u[I];L.push({t:F._utime,title:p.localeDate(F._utime),value:"Success (IP "+F.ipAddr+")"})}}if(t._loginHistory.failedLogin){z=t._loginHistory.failedLogin;for(H=0,w=z.length;H<w;H++){F=z[H];L.push({t:F._utime,title:p.localeDate(F._utime),value:F.error+" (IP "+F.ipAddr+")"})}}delete t._loginHistory;L.sort(function(Q,P){return Q.t-P.t});N.push({title:"__loginHistory__",nodes:L})}L=[];for(O in t){D=t[O];L.push({title:O,value:D})}L.sort(function(Q,P){if(Q.title>P.title){return 1}else{if(Q.title<P.title){return -1}else{return 0}}});N.push({title:"__attributesAndMacros__",nodes:L});return{_utime:v,id:B,nodes:N}};p.currentScope=r;s=r.$modelValue.session;m.get(scriptname+"sessions/"+o+"/"+s).then(function(t){return p.currentSession=q(t.data)});return p.showT=false};p.localeDate=function(q){var r;r=new Date(q*1000);return r.toLocaleString()};p.getLanguage=function(q){p.lang=q;p.form="white";p.init();return p.showM=false};k=function(r,q,s){var t;t=q.match(/#\/(\w+)/);o="global";if(t===null){p.type="_whatToTrace"}else{if(t[1].match(/^(persistent)$/)){o=RegExp.$1;p.type="_session_uid"}else{p.type=t[1]}}return p.init()};p.$on("$locationChangeSuccess",k);n=0;p.updateTree=function(y,s,q,v,x,u){var w,r,t;p.waiting=true;r=a[p.type]?a[p.type]:p.type==="_updateTime"?a._startTime:a._whatToTrace;w=r[q](p.type,y,x);if(u>b&&f[p.type]){if(t=f[p.type](p.type,y,q,v,x)){v++;w=t;q=q-1}else{v=0}}else{v=0}return m.get(scriptname+"sessions/"+o+"?"+w).then(function(A){var D,B,z,E,C;D=A.data;if(D.result){C=D.values;for(B=0,z=C.length;B<z;B++){E=C[B];n++;E.id="node"+n;if(q<r.length-1){E.nodes=[];E.level=q+1;E.query=w;E.over=v;if(p.type.match(/^(?:start|update)Time$/)){E.title=E.value.replace(/^(\d{8})(\d{2})(\d{2})$/,"$2:$3").replace(/^(\d{8})(\d{2})(\d)$/,"$2:$30").replace(/^(\d{8})(\d{2})$/,"$2h").replace(/^(\d{4})(\d{2})(\d{2})/,"$1-$2-$3")}}s.push(E)}if(y===""){p.total=D.total}}return p.waiting=false},function(z){return p.waiting=false})};p.init=function(){p.waiting=true;p.data=[];return j.all([h.init(p.lang),p.updateTree("",p.data,0,0)]).then(function(){return p.waiting=false},function(q){return p.waiting=false})};l=i.path().match(/^\/(\w+)/);return p.type=l?l[1]:"_whatToTrace"}])}).call(this);
\ No newline at end of file
(function(){var c,e,d,b,g,f,a;b=25;a={_whatToTrace:[function(i,h){return"groupBy=substr("+i+",1)"},function(i,h){return i+"="+h+"*&groupBy="+i},function(i,h){return i+"="+h}],ipAddr:[function(i,h){return"groupBy=net("+i+",16,1)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",32,2)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",48,3)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",128,4)"},function(i,h){return i+"="+h+"&groupBy=_whatToTrace"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],_startTime:[function(i,h){return"groupBy=substr("+i+",8)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",10)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",11)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",12)"},function(i,h){return i+"="+h+"*&groupBy=_whatToTrace"},function(i,h,j){console.log(i);console.log(h);console.log(j);return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],doubleIp:[function(i,h){return i},function(i,h){return"_whatToTrace="+h+"&groupBy=ipAddr"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&ipAddr="+h)}]};f={_whatToTrace:function(i,h,k,j){if(k===1){return i+"="+h+"*&groupBy=substr("+i+","+(k+j+1)+")"}else{return null}},ipAddr:function(i,h,k,j){if(k>0&&k<4){return i+"="+h+"*&groupBy=net("+i+","+(16*k+4*(j+1))+",2)"}else{return null}}};e="_password";c={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],BrowserID:["_browserIdAnswer","_browserIdAnswerRaw"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token"]};g={session:[{title:"deleteU2FKey",icon:"trash"}],home:[]};d=angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]);d.controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(p,h,i,j,m){var n,l,k,o;p.links=links;p.menulinks=menulinks;p.staticPrefix=staticPrefix;p.scriptname=scriptname;p.formPrefix=formPrefix;p.availableLanguages=availableLanguages;p.waiting=true;p.showM=false;p.showT=true;p.data=[];p.currentScope=null;p.currentSession=null;p.menu=g;p.translateP=h.translateP;p.translate=h.translate;p.translateTitle=function(q){return h.translateField(q,"title")};o="global";p.menuClick=function(q){if(q.popup){window.open(q.popup)}else{if(!q.action){q.action=q.title}switch(typeof q.action){case"function":q.action(p.currentNode,p);break;case"string":p[q.action]();break;default:console.log(typeof q.action)}}return p.showM=false};p.deleteU2FKey=function(){p.waiting=true;return m["delete"](scriptname+"u2f/"+o+"/"+p.currentSession.id).then(function(q){p.currentSession=null;p.currentScope.remove();return p.waiting=false},function(q){p.currentSession=null;p.currentScope.remove();return p.waiting=false})};p.stoggle=function(q){var r;r=q.$modelValue;if(r.nodes.length===0){p.updateTree(r.value,r.nodes,r.level,r.over,r.query,r.count)}return q.toggle()};p.displaySession=function(r){var s,q;q=function(t){var y,A,E,C,G,J,B,I,H,O,F,K,x,w,u,z,N,M,v,L,D;A=function(P){return P};y=function(S,U){var Q,R,P,T;P=[];R=new RegExp(S);for(Q in t){T=t[Q];if(Q.match(R)&&T){P.push({title:Q,value:T});delete t[Q]}}if(P.length>0){return N.push({title:U,nodes:P})}};v=t._utime;B=t._session_id;for(O in t){D=t[O];if(!D){delete t[O]}else{if(typeof t==="string"&&D.match(/; /)){t[O]=D.split("; ")}if(typeof t[O]!=="object"){if(e.match(new RegExp("\b"+O+"\b"))){t[O]="********"}else{if(O.match(/^(_utime|_lastAuthnUTime|_lastSeen|notification)$/)){t[O]=p.localeDate(D)}else{if(O.match(/^(_startTime|_updateTime)$/)){t[O]=A(D)}}}}}}N=[];for(G in c){C=c[G];M=[];for(J=0,K=C.length;J<K;J++){E=C[J];if(t[E]){M.push({title:E,value:t[E]});delete t[E]}}if(M.length>0){N.push({title:"__"+G+"__",nodes:M})}}y("^openid","OpenID");y("^notification_(.+)","__notificationsDone__");if(t._loginHistory){L=[];if(t._loginHistory.successLogin){u=t._loginHistory.successLogin;for(I=0,x=u.length;I<x;I++){F=u[I];L.push({t:F._utime,title:p.localeDate(F._utime),value:"Success (IP "+F.ipAddr+")"})}}if(t._loginHistory.failedLogin){z=t._loginHistory.failedLogin;for(H=0,w=z.length;H<w;H++){F=z[H];L.push({t:F._utime,title:p.localeDate(F._utime),value:F.error+" (IP "+F.ipAddr+")"})}}delete t._loginHistory;L.sort(function(Q,P){return Q.t-P.t});N.push({title:"__loginHistory__",nodes:L})}L=[];for(O in t){D=t[O];L.push({title:O,value:D})}L.sort(function(Q,P){if(Q.title>P.title){return 1}else{if(Q.title<P.title){return -1}else{return 0}}});N.push({title:"__attributesAndMacros__",nodes:L});return{_utime:v,id:B,nodes:N}};p.currentScope=r;s=r.$modelValue.session;m.get(scriptname+"sessions/"+o+"/"+s).then(function(t){return p.currentSession=q(t.data)});return p.showT=false};p.localeDate=function(q){var r;r=new Date(q*1000);return r.toLocaleString()};p.getLanguage=function(q){p.lang=q;p.form="white";p.init();return p.showM=false};k=function(r,q,s){var t;t=q.match(/#\/(\w+)/);o="global";if(t===null){p.type="_whatToTrace"}else{if(t[1].match(/^(persistent)$/)){o=RegExp.$1;p.type="_session_uid"}else{p.type=t[1]}}return p.init()};p.$on("$locationChangeSuccess",k);n=0;p.updateTree=function(y,s,q,v,x,u){var w,r,t;p.waiting=true;r=a[p.type]?a[p.type]:p.type==="_updateTime"?a._startTime:a._whatToTrace;w=r[q](p.type,y,x);if(u>b&&f[p.type]){if(t=f[p.type](p.type,y,q,v,x)){v++;w=t;q=q-1}else{v=0}}else{v=0}return m.get(scriptname+"sessions/"+o+"?"+w).then(function(A){var D,B,z,E,C;D=A.data;if(D.result){C=D.values;for(B=0,z=C.length;B<z;B++){E=C[B];n++;E.id="node"+n;if(q<r.length-1){E.nodes=[];E.level=q+1;E.query=w;E.over=v;if(p.type.match(/^(?:start|update)Time$/)){E.title=E.value.replace(/^(\d{8})(\d{2})(\d{2})$/,"$2:$3").replace(/^(\d{8})(\d{2})(\d)$/,"$2:$30").replace(/^(\d{8})(\d{2})$/,"$2h").replace(/^(\d{4})(\d{2})(\d{2})/,"$1-$2-$3")}}s.push(E)}if(y===""){p.total=D.total}}return p.waiting=false},function(z){return p.waiting=false})};p.init=function(){p.waiting=true;p.data=[];return j.all([h.init(p.lang),p.updateTree("",p.data,0,0)]).then(function(){return p.waiting=false},function(q){return p.waiting=false})};l=i.path().match(/^\/(\w+)/);return p.type=l?l[1]:"_whatToTrace"}])}).call(this);
\ No newline at end of file
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG U2F manager</title>
<title>LemonLDAP::NG sessions explorer</title>
</head>
<body ng-app="llngSessionsExplorer" ng-controller="SessionsExplorerCtrl" ng-csp>
......@@ -15,7 +15,6 @@
<div class="navbar navbar-default">
<div class="navbar-collapse">
<ul class="nav navbar-nav" role="grid">
<!--
<li uib-dropdown>
<a id="navsso" name="menu" uib-dropdown-toggle data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="glyphicon glyphicon-user"></i> {{translate('ssoSessions')}} <span class="caret"></span></a>
<ul uib-dropdown-menu aria-labelled-by="navsso">
......@@ -26,7 +25,6 @@
<li><a id="a-updatetime" href="#/_updateTime" role="row"><i class="glyphicon glyphicon-hourglass"></i> {{translate('_updateTime')}}</a></li>
</ul>
</li>
-->
<li><a id="a-persistent" href="#/persistent" role="row"><i class="glyphicon glyphicon-exclamation-sign"></i> {{translate('persistentSessions')}}</a></li>
</ul>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment