diff --git a/.gitmodules b/.gitmodules
index 6f9ccfa7037066bdeda69184d6e836d400cd97b5..bfc4ea1c918b16999a633bc4c949951e7a45d091 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -29,3 +29,6 @@
 [submodule "functions/parsedown"]
 	path = functions/parsedown
 	url = https://github.com/erusev/parsedown.git
+[submodule "functions/webauthn-php"]
+	path = functions/webauthn-php
+	url = https://github.com/Firehed/webauthn-php.git
diff --git a/app/admin/groups/edit-group-result.php b/app/admin/groups/edit-group-result.php
index 585abfd4c0833146dff396ffc7d3f07cc08cef04..9528d929a2ea465cee6436574251935ab7d8c9e4 100755
--- a/app/admin/groups/edit-group-result.php
+++ b/app/admin/groups/edit-group-result.php
@@ -93,6 +93,4 @@ if (!is_blank($_POST['gmembers'])) {
 			$Admin->add_group_to_user ($gid, $user->id);
 		}
 	}
-}
-
-?>
+}
\ No newline at end of file
diff --git a/app/admin/settings/index.php b/app/admin/settings/index.php
index 7c8d95a123d3e545a66c8604a7df70f3fb8ece29..fc1d69ded7c53bac636d2e8f3f44bd1af6e53ccc 100755
--- a/app/admin/settings/index.php
+++ b/app/admin/settings/index.php
@@ -552,6 +552,17 @@ $(document).ready(function() {
 	</td>
 </tr>
 
+<!-- Passkeys -->
+<tr>
+	<td class="title"><?php print _("Enable Passkeys"); ?></td>
+	<td>
+		<input type="checkbox" class="input-switch" value="1" name="passkeys" <?php if($settings['passkeys'] == 1) print 'checked'; ?>>
+	</td>
+	<td class="info2">
+		<?php print _('Enable passkeys for passwordless login'); ?>
+	</td>
+</tr>
+
 
 <!-- ICPM -->
 <tr class="settings-title">
diff --git a/app/admin/settings/settings-save.php b/app/admin/settings/settings-save.php
index 4526465322797d595c94c3af363e88c177ad6848..5a0797fec97ad392d649d99f42c31aa379790ff9 100755
--- a/app/admin/settings/settings-save.php
+++ b/app/admin/settings/settings-save.php
@@ -87,6 +87,7 @@ $values = array("id"=>1,
 				"enforceUnique"       =>$Admin->verify_checkbox(@$_POST['enforceUnique']),
 				"enableRouting"       =>$Admin->verify_checkbox(@$_POST['enableRouting']),
 				"enableVaults"        =>$Admin->verify_checkbox(@$_POST['enableVaults']),
+				"passkeys"            =>$Admin->verify_checkbox(@$_POST['passkeys']),
 				//"enableDHCP"        =>$Admin->verify_checkbox(@$_POST['enableDHCP']),
 				"enableFirewallZones" =>$Admin->verify_checkbox(@$_POST['enableFirewallZones']),
 				"maintaneanceMode" 	  =>$Admin->verify_checkbox(@$_POST['maintaneanceMode']),
diff --git a/app/admin/users/edit-result.php b/app/admin/users/edit-result.php
index 041e08395d662f9dd55065fcdd8ab9edcd2d239d..38a59bacd4ab1dcf37bf67080a8b743549d348ee 100755
--- a/app/admin/users/edit-result.php
+++ b/app/admin/users/edit-result.php
@@ -31,7 +31,6 @@ $User->Crypto->csrf_cookie ("validate", "user", $_POST['csrf_cookie']) === false
 $auth_method = $Admin->fetch_object ("usersAuthMethod", "id", $_POST['authMethod']);
 $auth_method!==false ? : $Result->show("danger", _("Invalid authentication method"), true);
 
-
 /* checks */
 
 # ID must be numeric
@@ -106,7 +105,6 @@ if(sizeof($myFields) > 0) {
 $values = array(
 				"id"             =>@$_POST['userId'],
 				"real_name"      =>$_POST['real_name'],
-				"username"       =>$_POST['username'],
 				"email"          =>$_POST['email'],
 				"role"           =>$_POST['role'],
 				"authMethod"     =>$_POST['authMethod'],
@@ -118,6 +116,10 @@ $values = array(
 				);
 
 
+# username only on add
+if($_POST['action']=="add") {
+	$values['username'] = $_POST['username'];
+}
 
 # custom fields
 if (sizeof($myFields)>0) {
@@ -161,6 +163,27 @@ foreach ($User->get_modules_with_permissions() as $m) {
 # formulate permissions
 $values['module_permissions'] = json_encode($permissions);
 
+# 2fa
+if ($User->settings->{'2fa_provider'}!=='none') {
+	if(!isset($_POST['2fa'])) {
+		$values['2fa']        = 0;
+		$values['2fa_secret'] = NULL;
+	}
+}
+
+# passkeys
+$passkeys_to_remove = [];
+foreach($_POST as $key=>$post) {
+	if(substr($key, 0,15) == "delete-passkey-") {
+		$passkeys_to_remove[] = str_replace("delete-passkey-", "", $key);
+	}
+}
+
+# passkey only
+if ($User->settings->{'passkeys'}==1) {
+	$values['passkey_only'] = !isset($_POST['passkey_only']) ? 0 : 1;
+}
+
 # execute
 if(!$Admin->object_modify("users", $_POST['action'], "id", $values)) {
     $Result->show("danger", _("User")." ".$_POST["action"]." "._("failed").'!', true);
@@ -169,5 +192,13 @@ else {
     $Result->show("success", _("User")." ".$_POST["action"]." "._("successful").'!', false);
 }
 
+# remove passkeys if required
+if (sizeof($passkeys_to_remove)>0) {
+	// lalala
+	foreach ($passkeys_to_remove as $pk) {
+		$User->delete_passkey ($pk);
+	}
+}
+
 # mail user
 if($Admin->verify_checkbox(@$_POST['notifyUser'])!="0") { include("edit-notify.php"); }
diff --git a/app/admin/users/edit.php b/app/admin/users/edit.php
index 9cb461a3e4094b9f6c8d48a681bcc6c72c130011..b85c92f3830d369cd69b478e9df1d16023034f25 100755
--- a/app/admin/users/edit.php
+++ b/app/admin/users/edit.php
@@ -51,6 +51,12 @@ else {
 	//set default lang
 	$user['lang']=$User->settings->defaultLang;
 }
+
+# disabled
+$disabled = $_POST['action']=="delete" ? "disabled" : "";
+
+# passkeys
+$user_passkeys = $User->get_user_passkeys($user['id']);
 ?>
 
 <script>
@@ -84,16 +90,18 @@ $(document).ready(function(){
 	<!-- real name -->
 	<tr>
 	    <td><?php print _('Real name'); ?></td>
-	    <td><input type="text" class="form-control input-sm" name="real_name" value="<?php print @$user['real_name']; ?>"></td>
+	    <td><input type="text" class="form-control input-sm" name="real_name" value="<?php print @$user['real_name']; ?>" <?php print $disabled; ?>></td>
        	<td class="info2"><?php print _('Enter users real name'); ?></td>
     </tr>
 
     <!-- username -->
     <tr>
     	<td><?php print _('Username'); ?></td>
-    	<td><input type="text" class="form-control input-sm" name="username" value="<?php print @$user['username']; ?>" <?php if($_POST['action']=="edit"||$_POST['action']=="delete") print 'readonly'; ?>></td>
+    	<td><input type="text" class="form-control input-sm" name="username" value="<?php print @$user['username']; ?>" <?php if($_POST['action']=="edit"||$_POST['action']=="delete") print 'readonly disabled'; ?> <?php print $disabled; ?>></td>
     	<td class="info2">
+    		<?php if($_POST['action']=="add") { ?>
     		<a class='btn btn-xs btn-default adsearchuser' rel='tooltip' title='Search AD for user details'><i class='fa fa-search'></i></a>
+    		<?php } ?>
 			<?php print _('Enter username'); ?>
 		</td>
     </tr>
@@ -101,10 +109,16 @@ $(document).ready(function(){
     <!-- email -->
     <tr>
     	<td><?php print _('e-mail'); ?></td>
-    	<td><input type="text" class="form-control input-sm input-w-250" name="email" value="<?php print @$user['email']; ?>"></td>
+    	<td><input type="text" class="form-control input-sm input-w-250" name="email" value="<?php print @$user['email']; ?>" <?php print $disabled; ?>></td>
     	<td class="info2"><?php print _('Enter users email address'); ?></td>
     </tr>
 
+    <?php if($_POST['action']!="delete") { ?>
+
+	<tr>
+		<td colspan="3"><hr></td>
+	</tr>
+
     <!-- Status -->
     <tr>
     	<td><?php print _('Status'); ?></td>
@@ -159,6 +173,42 @@ $(document).ready(function(){
 		<td class="info2"><?php print _("Select authentication method for user"); ?></td>
 	</tr>
 
+	<?php if ($User->settings->{'2fa_provider'}!=='none' && $user['2fa'] == "1") { ?>
+
+    <tr>
+    	<td style="padding-top:10px;"><?php print _('2fa enabled'); ?></td>
+    	<td style="padding-top:10px;"><input type="checkbox" value="1" class="input-switch" name="2fa" <?php if($user['2fa'] == "1") { print 'checked'; } else { print "disabled"; } ?>></td>
+    	<td style="padding-top:10px;" class="info2"><?php print _('Disable 2fa for user'); ?></td>
+    </tr>
+	<?php } ?>
+
+
+	<?php if ($User->settings->{'passkeys'}=="1" && sizeof($user_passkeys)>0 && $_POST['action']!=="delete") { ?>
+	<tr>
+		<td colspan="3"><hr></td>
+	    <tr>
+	    	<td style="padding-top:10px;"><?php print _('Passkeys'); ?></td>
+	    	<td style="padding-top:10px;">
+	    	<?php
+	    	foreach ($user_passkeys as $passkey) {
+	    		$passkey->comment = is_null($passkey->comment) ? "-- Unknown --" : $passkey->comment;
+	    		print "<input type='checkbox' name='delete-passkey-".$passkey->id."' value='1'> ";
+	    		print $User->strip_input_tags($passkey->comment)."<br>";
+	    	}
+	    	?>
+	    	</td>
+	    	<td style="padding-top:10px;" class="info2"><?php print _('Check passkey you want to remove'); ?></td>
+	    </tr>
+
+	    <tr>
+	    	<td style="padding-top:10px;"><?php print _('Passkey login only'); ?></td>
+    		<td style="padding-top:10px;"><input type="checkbox" value="1" class="input-switch" name="passkey_only" <?php if($user['passkey_only'] == "1") { print 'checked'; } ?>></td>
+	    	<td style="padding-top:10px;" class="info2"><?php print _('Select to only allow account login with passkey'); ?></td>
+	    </tr>
+	</tr>
+
+	<?php } ?>
+
 	<tr>
 		<td colspan="3"><hr></td>
 	</tr>
@@ -407,6 +457,7 @@ $(document).ready(function(){
 	}
 	?>
 
+	<?php } ?>
 
 </table>
 </form>
diff --git a/app/admin/users/print-all.php b/app/admin/users/print-all.php
index 1fe6ee0efb64442aeb4733c910a2792d7d331670..89a2c469d4addab191ce3a23f6fa014d308f0ce2 100644
--- a/app/admin/users/print-all.php
+++ b/app/admin/users/print-all.php
@@ -12,7 +12,7 @@ $users = $Admin->fetch_all_objects("users", "username");
 # fetch custom fields
 $custom = $Tools->fetch_custom_fields('users');
 
-/* check customfields */
+// check customfields
 $ffields = pf_json_decode($User->settings->hiddenCustomFields, true);
 $ffields = is_array(@$ffields['users']) ? $ffields['users'] : array();
 ?>
@@ -60,6 +60,15 @@ $ffields = is_array(@$ffields['users']) ? $ffields['users'] : array();
 foreach ($users as $user) {
 	//cast
 	$user = (array) $user;
+
+	// passkeys
+	if ($User->settings->{'passkeys'}=="1") {
+		// get user passkeys
+		$user_passkeys = $User->get_user_passkeys($user['id']);
+		// set passkey_only flag
+		$passkey_only = $User->settings->{'passkeys'}=="1" && sizeof($user_passkeys)>0 && $user['passkey_only']=="1" ? true : false;
+	}
+
 	print '<tr>' . "\n";
 
 	# set icon based on normal user or admin
@@ -88,8 +97,27 @@ foreach ($users as $user) {
 	$auth_method = $Admin->fetch_object("usersAuthMethod", "id", $user['authMethod']);
 	//false
 	print "<td>";
-	if($auth_method===false) { print "<span class='text-muted'>No auth method</span>"; }
-	else 					 { print $auth_method->type." <span class='text-muted'>(".$auth_method->description."</a>)"; }
+	if($auth_method===false) 	{ print "<span class='text-muted'>No auth method</span>"; }
+	elseif($passkey_only)   	{ print "<span class='badge badge1 badge5 alert-success'>"._("Passkey only")."</span>"; }
+	else 					 	{ print "<span class='badge badge1 badge5 alert-success'>".$auth_method->type."</span> <span class='text-muted'>(".$auth_method->description."</a>)"; }
+	// 2fa
+	if ($User->settings->{'2fa_provider'}!=='none' && $passkey_only!==true) {
+		if (!is_null($user['2fa_secret']) && $user['2fa']=="1") {
+			print "<br><span class='badge badge1 badge5 alert-success'>"._("2fa enabled")."</span>";
+		}
+		else {
+			print "<br><span class='badge badge1 badge5 alert-warning'>"._("2fa disabled")."</span>";
+		}
+	}
+
+	// passkeys
+	if ($User->settings->{'passkeys'}=="1") {
+		// get user passkeys
+		$user_passkeys = $User->get_user_passkeys($user['id']);
+		if (sizeof($user_passkeys)>0) {
+			print "<br><span class='badge badge1 badge5 alert-success'>".sizeof($user_passkeys)." "._("Passkeys")."</span>";
+		}
+	}
 	print "</span></td>";
 
 	# Module permisisons
@@ -98,7 +126,9 @@ foreach ($users as $user) {
 	}
 	else {
 		print "<td>";
+		print "<btn class='btn btn-xs btn-default toggle-module-permissions'>Show <i class='fa fa-angle-down'></i></btn><div class='hidden module-permissions'>";
 		include("print_module_permissions.php");
+		print "</div>";
 		print "</td>";
 	}
 
diff --git a/app/admin/users/print_module_permissions.php b/app/admin/users/print_module_permissions.php
index c223fac25ff1792ecd0eaebaca9c0921f4378033..4aceef18f61253f5b375fdd709cceea76c63baac 100644
--- a/app/admin/users/print_module_permissions.php
+++ b/app/admin/users/print_module_permissions.php
@@ -23,55 +23,76 @@ foreach ($User->get_modules_with_permissions() as $m) {
     }
 }
 
-print "<table class='table-noborder popover_table'>";
-
 
 // VLAN
-print "<tr><td>"._("VLAN")."</td><td>".$User->print_permission_badge($user['perm_vlan'])."</td></tr>";
-
+$perm_names['perm_vlan'] = "VLAN";
 // L2Domains
-print "<tr><td>"._("L2Domains")."</td><td>".$User->print_permission_badge($user['perm_l2dom'])."</td></tr>";
-
+$perm_names['perm_l2dom'] = "L2 Domains";
 // VRF
-print "<tr><td>"._("VRF")."</td><td>".$User->print_permission_badge($user['perm_vrf'])."</td></tr>";
-
+$perm_names['perm_vrf'] = "VRF";
 // PDNS
 if ($User->settings->enablePowerDNS==1)
-print "<tr><td>"._("PowerDNS")."</td><td>".$User->print_permission_badge($user['perm_pdns'])."</td></tr>";
-
+$perm_names['perm_pdns'] = "PowerDNS";
 // Devices
-print "<tr><td>"._("Devices")."</td><td>".$User->print_permission_badge($user['perm_devices'])."</td></tr>";
-
+$perm_names['perm_devices'] = "Devices";
 // Racks
 if ($User->settings->enableRACK==1)
-print "<tr><td>"._("Racks")."</td><td>".$User->print_permission_badge($user['perm_racks'])."</td></tr>";
-
+$perm_names['perm_racks'] = "Racks";
 // Circuits
 if ($User->settings->enableCircuits==1)
-print "<tr><td>"._("Circuits")."</td><td>".$User->print_permission_badge($user['perm_circuits'])."</td></tr>";
-
+$perm_names['perm_circuits'] = "Circuits";
 // NAT
 if ($User->settings->enableNAT==1)
-print "<tr><td>"._("NAT")."</td><td>".$User->print_permission_badge($user['perm_nat'])."</td></tr>";
-
+$perm_names['perm_nat'] = "NAT";
 // Customers
 if ($User->settings->enableCustomers==1)
-print "<tr><td>"._("Customers")."</td><td>".$User->print_permission_badge($user['perm_customers'])."</td></tr>";
-
+$perm_names['perm_customers'] = "Customers";
 // Locations
 if ($User->settings->enableLocations==1)
-print "<tr><td>"._("Locations")."</td><td>".$User->print_permission_badge($user['perm_locations'])."</td></tr>";
-
+$perm_names['perm_locations'] = "Locations";
 // pstn
 if ($User->settings->enablePSTN==1)
-print "<tr><td>"._("PSTN")."</td><td>".$User->print_permission_badge($user['perm_pstn'])."</td></tr>";
-
+$perm_names['perm_pstn'] = "PSTN";
 // routing
 if ($User->settings->enableRouting==1)
-print "<tr><td>"._("Routing")."</td><td>".$User->print_permission_badge($user['perm_routing'])."</td></tr>";
-
+$perm_names['perm_routing'] = "Routing";
 // vaults
 if ($User->settings->enableVaults==1)
-print "<tr><td>"._("Vaults")."</td><td>".$User->print_permission_badge($user['perm_vaults'])."</td></tr>";
-
-print "</table>";
\ No newline at end of file
+$perm_names['perm_vaults'] = "Vaults";
+
+
+// user page
+if((@$_GET['page']=="administration" && @$_GET['section']=="users" && @$_GET['sPage']=="modules") || ($_GET['section']=="user-menu")) {
+
+    print '<div class="panel panel-default" style="max-width:600px;min-width:350px;">';
+    print '<div class="panel-heading">'._("User permissions for phpipam modules").'</div>';
+    print ' <ul class="list-group">';
+
+    foreach ($user as $key=>$u) {
+        if(strpos($key, "perm_")!==false && array_key_exists($key, $perm_names)) {
+            print '<li class="list-group-item">';
+            // title
+            print "<span style='padding-top:8px;' class='pull-l1eft'>";
+            print "<strong>"._($perm_names[$key])."</strong>";
+            print "</span>";
+            // perms
+            print ' <strong class="btn-group pull-right">';
+            print $User->print_permission_badge($user[$key]);
+            print ' </strong>';
+            print '</li>';
+
+            print "<div class='clearfix'></div>";
+        }
+    }
+    print ' </ul>';
+    print '</div>';
+}
+else {
+    print "<table class='table-noborder popover_table'>";
+    foreach ($user as $key=>$u) {
+        if(strpos($key, "perm_")!==false && array_key_exists($key, $perm_names)) {
+            print "<tr><td>"._($perm_names[$key])."</td><td>".$User->print_permission_badge($user[$key])."</td></tr>";
+        }
+    }
+    print "</table>";
+}
\ No newline at end of file
diff --git a/app/login/index.php b/app/login/index.php
index 403e2c7ffeee30bbf10515e24e33e8a84c827d7b..7f5c2b3b66f92106df81df7a6eac400cf4b83448 100755
--- a/app/login/index.php
+++ b/app/login/index.php
@@ -133,41 +133,6 @@ if(@$config['requests_public']===false) {
 	else 										{ $_GET['subnetId'] = "404"; print "<div id='error'>"; include_once('app/error.php'); print "</div>"; }
 	?>
 
-	<!-- login response -->
-	<div id="loginCheck">
-		<?php
-		# deauthenticate user
-		if ( $User->is_authenticated()===true ) {
-			# print result
-			if(isset($_GET['section']) && $_GET['section']=="timeout")
-				$Result->show("success", _('You session has timed out'));
-			else
-				$Result->show("success", _('You have logged out'));
-
-			# write log
-			$Log->write( _("User logged out"), _("User")." ".$User->username." "._("has logged out"), 0, $User->username );
-
-			# destroy session
-			$User->destroy_session();
-		}
-
-		//check if SAML2 login is possible
-		$saml2settings=$Tools->fetch_object("usersAuthMethod", "type", "SAML2");
-
-		if ($saml2settings!=false) {
-			$version = pf_json_decode(@file_get_contents(dirname(__FILE__).'/../../functions/php-saml/src/Saml2/version.json'), true);
-			$version = $version['php-saml']['version'];
-
-			if ($version < 3.4) {
-				$Result->show("danger", _('php-saml library missing, please update submodules'));
-			} else {
-				$Result->show("success", _('You can login with SAML2').' <a href="'.create_link('saml2').'">'._('here').'</a>!');
-			}
-		}
-
-		?>
-	</div>
-
 </div>
 </div>
 
diff --git a/app/login/login_form.php b/app/login/login_form.php
index 366d4d3d5df28eedd50726e5021f79447b8dd3c0..072df17b670a8359e64b90679eddf3a231dca31f 100755
--- a/app/login/login_form.php
+++ b/app/login/login_form.php
@@ -46,11 +46,75 @@
 	</div>
 	<?php } ?>
 
-	<div class="col-xs-12">
-		<hr>
-		<input type="submit" value="<?php print _('Login'); ?>" class="btn btn-sm btn-default pull-right"></input>
+	<div class="col-xs-12" style="padding-top:15px;">
+		<!-- <hr style="margin-top:5px;margin-bottom:10px;"> -->
+		<input type="submit" value="<?php print _('Login'); ?>" class="btn btn-sm btn-success" style="width:100%"></input>
 	</div>
 
+
+	<!-- login response -->
+	<div id="loginCheck" class="col-xs-12 text-center">
+		<?php
+		# deauthenticate user
+		if ( $User->is_authenticated()===true ) {
+			# print result
+			if(isset($_GET['section']) && $_GET['section']=="timeout")
+				$Result->show("success", _('You session has timed out'));
+			else
+				$Result->show("success", _('You have logged out'));
+
+			# write log
+			$Log->write( _("User logged out"), _("User")." ".$User->username." "._("has logged out"), 0, $User->username );
+
+			# destroy session
+			$User->destroy_session();
+		}
+
+		//check if SAML2 login is possible
+		$saml2settings=$Tools->fetch_object("usersAuthMethod", "type", "SAML2");
+
+		if ($saml2settings!=false) {
+			$version = pf_json_decode(@file_get_contents(dirname(__FILE__).'/../../functions/php-saml/src/Saml2/version.json'), true);
+			$version = $version['php-saml']['version'];
+
+			if ($version < 3.4) {
+				$Result->show("danger", _('php-saml library missing, please update submodules'));
+			} else {
+				$Result->show("success", _('You can login with SAML2').' <a href="'.create_link('saml2').'">'._('here').'</a>!');
+			}
+		}
+
+		?>
+	</div>
+
+
+	<?php if($User->settings->{'passkeys'}=="1") { ?>
+	<div class="col-xs-12" style="padding-top:20px;">
+
+		<div style="width: 45%;" class='text-center pull-left'>
+			<hr style="padding-top: 3px">
+		</div>
+		<div style="width: 10%;" class='text-center pull-left'>
+			or
+		</div>
+		<div style="width: 45%;" class='text-center pull-left'>
+			<hr style="padding-top: 3px">
+		</div>
+
+		<button class="btn btn-sm btn-default passkey_login" style="width:100%;margin-top:20px;">
+			<svg height="14" aria-hidden="true" viewBox="0 -3 32 24" version="1.1" width="20" data-view-component="true" class="octicon octicon-passkey-fill">
+    			<path d="M9.496 2a5.25 5.25 0 0 0-2.519 9.857A9.006 9.006 0 0 0 .5 20.228a.751.751 0 0 0 .728.772h5.257c3.338.001 6.677.002 10.015 0a.5.5 0 0 0 .5-.5v-4.669a.95.95 0 0 0-.171-.551 9.02 9.02 0 0 0-4.814-3.423A5.25 5.25 0 0 0 9.496 2Z"></path>
+    			<path d="M23.625 10.313c0 1.31-.672 2.464-1.691 3.134a.398.398 0 0 0-.184.33v.886a.372.372 0 0 1-.11.265l-.534.534a.188.188 0 0 0 0 .265l.534.534c.071.07.11.166.11.265v.347a.374.374 0 0 1-.11.265l-.534.534a.188.188 0 0 0 0 .265l.534.534a.37.37 0 0 1 .11.265v.431a.379.379 0 0 1-.097.253l-1.2 1.319a.781.781 0 0 1-1.156 0l-1.2-1.319a.379.379 0 0 1-.097-.253v-5.39a.398.398 0 0 0-.184-.33 3.75 3.75 0 1 1 5.809-3.134ZM21 9.75a1.125 1.125 0 1 0-2.25 0 1.125 1.125 0 0 0 2.25 0Z"></path>
+			</svg>
+			<span>
+			<?php print _("Login with a passkey"); ?>
+			</span>
+		</button>
+
+	</div>
+	<div id="loginCheckPasskeys" class="col-xs-12 text-center"></div>
+	<?php } ?>
+
 </div>
 
 </form>
diff --git a/app/login/passkey_login_check.php b/app/login/passkey_login_check.php
new file mode 100644
index 0000000000000000000000000000000000000000..af58b038a6765ea7e327d3164a7306f8eb7f6f97
--- /dev/null
+++ b/app/login/passkey_login_check.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ *
+ * Save users passkey
+ *
+ */
+
+
+# include composer
+require __DIR__ . '/../../functions/vendor/autoload.php';
+
+// phpipam stuff
+require_once( dirname(__FILE__) . '/../../functions/functions.php' );
+
+# initialize required objects
+$Database       = new Database_PDO;
+$User           = new User ($Database);
+
+// set header typw
+header('Content-Type: text/html; charset=utf-8');
+
+// webauthn modules
+use Firehed\WebAuthn\{
+    ChallengeManagerInterface,
+    Codecs,
+    CredentialContainer,
+    RelyingParty,
+    SessionChallengeManager,
+    SingleOriginRelyingParty,
+    ResponseParser,
+    BinaryString
+};
+
+// process request json
+$json = file_get_contents('php://input');
+$data = json_decode($json, true);
+
+
+// parser
+$parser = new ResponseParser();
+$getResponse = $parser->parseGetResponse($data);
+
+// header('HTTP/1.1 201 Created');
+// header('Content-Type: text/html; charset=utf-8');
+// echo(implode('', array_map('chr', $data['keyId'])));
+// die();
+
+// Set relaying party
+$rp = new \Firehed\WebAuthn\SingleOriginRelyingParty('https://ipam-dc.ugbb.net');
+// challange manager
+$challengeManager = new \Firehed\WebAuthn\SessionChallengeManager();
+
+// get user credentials
+$passkey = $User->get_user_passkey_by_keyId ($data['keyId']);
+
+// none found
+if (is_null($passkey)) {
+    header('HTTP/1.1 404 Not Found');
+    return;
+}
+else {
+    try {
+        // set user id
+        $User->set_passkey_user_id ($passkey->user_id);
+
+        // init credentails
+        $codec = new Codecs\Credential();
+
+        // set credentials
+        $credentials[0] = $codec->decode($passkey->credential);;
+
+        // container
+        $credentialContainer = new CredentialContainer($credentials);
+
+        // Verify credentials, if it fails exit
+        $updatedCredential = $getResponse->verify($challengeManager, $rp, $credentialContainer);
+
+        // Auth success. Now update credential and save session
+
+        // encode credentials to store to database
+        $codec = new Codecs\Credential();
+        $encodedCredential = $codec->encode($updatedCredential);
+
+        // confirm login
+        $User->auth_passkey ($updatedCredential->getStorageId(), $encodedCredential, $data['keyId']);
+
+        // print result
+        header('HTTP/1.1 200 OK');
+        header('Content-type: application/json');
+        echo json_encode([
+                'success' => true,
+                'credential_ids' => $updatedCredential->getStorageId()
+        ]);
+    }
+    catch (Exception $e) {
+        header('HTTP/1.1 500 '.$e->getMessage());
+    }
+}
\ No newline at end of file
diff --git a/app/subnets/subnet-details/subnet-map-vertical.php b/app/subnets/subnet-details/subnet-map-vertical.php
new file mode 100644
index 0000000000000000000000000000000000000000..538ecbe196361425486f3475f144bb7d7a1691af
--- /dev/null
+++ b/app/subnets/subnet-details/subnet-map-vertical.php
@@ -0,0 +1,153 @@
+<?php
+
+# array
+$free_subnets = [];
+
+
+// ipv6
+if($Tools->identify_address($subnet['subnet'])=="IPv6") {
+	$maxmask = $subnet['mask']+10>128 ? 128 : $subnet['mask']+10;
+	$pow = 128;
+}
+else {
+	$maxmask = $subnet['mask']+10>32 ? 32 : $subnet['mask']+10;
+	$pow = 32;
+}
+
+
+# reset if search
+if(@$from_search===true) {
+	$maxmask = $_GET['ipaddrid']+1;
+	$subnetmask = $_GET['ipaddrid']-1;
+}
+else {
+	$subnetmask = $subnet['mask'];
+}
+
+// print $subnet['mask'];
+// print $maxmask;
+
+# create free objects
+for($searchmask=$subnetmask+1; $searchmask<$maxmask; $searchmask++) {
+	$found = $Subnets->search_available_subnets ($subnet['id'], $searchmask, $count = Subnets::SEARCH_FIND_ALL, $direction = Subnets::SEARCH_FIND_FIRST);
+	if($found!==false) {
+		// check if subnet has addresses
+		if($Addresses->count_subnet_addresses ($subnet['id'])>0) {
+			// subnet aqddresses
+			$subnet_addresses = $Addresses->fetch_subnet_addresses ($subnet['id'], null, null, $fields = ['ip_addr']);
+
+			// remove found subnets with hosts !
+			foreach($found as $k=>$f) {
+				// parse
+				$parsed = explode("/", $f);
+				// boundaries
+				$boundaries = $Subnets->get_network_boundaries ($parsed[0], $searchmask);
+				// broadcast to int
+				$maxint = isset($boundaries['broadcast']) ? $Subnets->transform_address ($boundaries['broadcast'],"decimal") : 0;
+
+				if(sizeof($subnet_addresses)>0) {
+					foreach ($subnet_addresses as $a) {
+						if ($a->ip_addr>=$Subnets->transform_address($parsed[0],"decimal") && $a->ip_addr<=$maxint ) {
+							unset($found[$k]);
+						}
+					}
+				}
+			}
+
+			// save remaining
+			$free_subnets[$searchmask] = $found;
+		}
+		else {
+			$free_subnets[$searchmask] = $found;
+		}
+	}
+	else {
+		$free_subnets[$searchmask] = [];
+	}
+}
+
+# if some found print
+if (sizeof($free_subnets)>0) {
+
+	// get maximum number of subnets that will be calculated
+	$max_all_subnets = pow(2,array_keys($free_subnets)[count($free_subnets)-1]-$subnet['mask']);
+	$levels = sizeof($free_subnets);
+
+	// content
+	print "<div id='showFreeSubnets'>";
+
+	// table
+	print "<table>";
+
+	// headers
+	print "<tr>";
+	foreach ($free_subnets as $free_mask=>$items) {
+	print "	<td>/".$free_mask."</td>";
+	}
+	print "</tr>";
+
+
+	$all_keys = array_keys($free_subnets);
+
+
+	for($m=0; $m<=$max_all_subnets;$m++) {
+
+		// save start
+		$subnet_start = $subnet['subnet'];
+
+		print "<tr>";
+		foreach ($all_keys as $array_key) {
+			// max subnets
+			$max_subnets = pow(2,$array_key-$subnet['mask']);
+
+
+				if(in_array($Subnets->transform_address($subnet_start, "dotted")."/".$array_key, $free_subnets[$array_key])) {
+					print "<td>".$free_subnets[$array_key][$m]."/".$array_key."</td>";
+				}
+				else {
+					print "<td>/</td>";
+				}
+
+				// next subnet
+				$subnet_start = gmp_strval(gmp_add($subnet_start, gmp_pow(2, ($pow-$array_key))));
+
+
+				// rowspan
+				// $rowspan = $max_all_subnets/$max_subnets;
+
+		}
+		print "</tr>";
+	}
+
+
+
+	// items
+	foreach ($free_subnets as $free_mask=>$items) {
+		break;
+
+		// max
+		$max_subnets = pow(2,$free_mask-$subnet['mask']);
+
+		// save start
+		$subnet_start = $subnet['subnet'];
+
+		// print
+		print "<div class='ip_vis_subnet'>";
+		for($m=1; $m<=$max_subnets;$m++) {
+			if(in_array($Subnets->transform_address($subnet_start, "dotted")."/".$free_mask, $items)) {
+				print "<span class='subnet_map subnet_map_$pow subnet_map_found'><a href='' data-sectionid='".$section['id']."' data-mastersubnetid='".$subnet['id']."' class='createfromfree' data-cidr='".$Subnets->transform_address($subnet_start, "dotted")."/".$free_mask."' rel='tooltip' title='"._("Create subnet")."'>".$Subnets->transform_address($subnet_start, "dotted")."/".$free_mask."</a></span>";
+			}
+			else {
+				print "<span class='subnet_map subnet_map_$pow subnet_map_notfound'>".$Subnets->transform_address($subnet_start, "dotted")."/".$free_mask."</span>";
+			}
+
+			// next subnet
+			// $subnet_start = $subnet_start + pow(2,($pow-$free_mask));
+			$subnet_start = gmp_strval(gmp_add($subnet_start, gmp_pow(2, ($pow-$free_mask))));
+
+		}
+	}
+
+	print "</table>";
+	print "</div>";
+}
\ No newline at end of file
diff --git a/app/tools/user-menu/2fa.php b/app/tools/user-menu/2fa.php
index d7a7eadf70e70275b462866aa89e15534b978502..da5cd012de4f3cc64194c052227f9a68cd44a7d6 100644
--- a/app/tools/user-menu/2fa.php
+++ b/app/tools/user-menu/2fa.php
@@ -22,22 +22,37 @@ if (is_null($User->user->{'2fa_secret'}) && $User->user->{'2fa'}=="1") {
 
 // get QR code
 $username = strtolower($User->user->username)."@".$User->settings->{'2fa_name'};
+
+// passkey only
+if ($User->settings->{'passkeys'}=="1") {
+	// get user passkeys
+	$user_passkeys = $User->get_user_passkeys($User->user->id);
+	// set passkey_only flag
+	$passkey_only = $User->settings->{'passkeys'}=="1" && sizeof($user_passkeys)>0 && $User->user->passkey_only=="1" ? true : false;
+}
 ?>
 
+
+
 <h4><?php print _('Two-factor authentication'); ?></h4>
 <hr>
 <span class="info2"><?php print _("Here you can change settings for two-factor authentication and get your 2fa secret."); ?></span>
 <br><br>
 
+<?php if(!$passkey_only) { ?>
+<div class="panel panel-default" style="max-width:300px;min-width:350px;">
+<ul class="list-group">
+<div class="panel-heading"><?php print _('2fa account status'); ?></div>
+<li class="list-group-item">
 <form name="2fa_user" id="2fa_user">
-<table id="userModSelf" class="table table-condensed">
+<table id="userModSelf" class="table table-condensed" style='margin-bottom:0px;width:100%'>
 <tr>
 	<td class="title"><?php print _('2fa status'); ?></td>
 	<?php if ($User->settings->{'2fa_userchange'}=="1") { ?>
 	<td>
 		<input type="checkbox" value="1" class="input-switch" name="2fa" <?php if($User->user->{'2fa'} == 1) print 'checked'; ?>>
 	</td>
-	<td>
+	<td class="text-right">
 		<input type="submit" class="btn btn-default btn-success btn-sm submit_popup" data-script="app/tools/user-menu/2fa_save.php" data-result_div="userModSelf2faResult" data-form='2fa_user' value="<?php print _("Save"); ?>">
 	</td>
 	<?php } else { ?>
@@ -51,22 +66,28 @@ $username = strtolower($User->user->username)."@".$User->settings->{'2fa_name'};
 </table>
 <input type="hidden" name="csrf_cookie" value="<?php print $csrf; ?>">
 </form>
+</li>
+</ul>
+</div>
+<?php } ?>
+
 
 <!-- result -->
 <div id="userModSelf2faResult" style="margin-bottom:90px;display:none"></div>
 
-
-<hr>
-<br><br>
 <?php
 
-if($User->user->{'2fa_secret'}!=null) {
+if ($passkey_only) {
+	$Result->show ("warning alert-absolute", _("You can only login to your account using passkeys").".", false);
+}
+elseif($User->user->{'2fa_secret'}!=null && $User->user->{'2fa'}==1) {
 	$html   = [];
-	$html[] = '<div class="loginForm row" style="width:400px;">';
+	$html[] = "<hr><br>";
+	$html[] = '<div class="loginForm row" style="width:500px;">';
 	$html[] = '		'._('Details for your preferred authenticator application are below. Please write down your details, otherwise you will not be able to login to phpipam').".";
 	$html[] = '		<div style="border: 2px dashed red;margin:20px;padding: 10px" class="text-center row">';
-	$html[] = '			<div class="col-xs-12" style="padding:5px 10px 3px 20px;"><strong>'._('Account').': <span style="color:red; font-size: 16px">'.$username.'</span></strong></div>';
-	$html[] = '			<div class="col-xs-12" style="padding:0px 10px 3px 20px;"><strong>'._('Secret').' : <span style="color:red; font-size: 16px">'.$User->user->{'2fa_secret'}.'</span></strong></div>';
+	$html[] = '			<div class="col-xs-12" style="padding:5px 10px 3px 20px;"><strong>'._('Account').':<br> <span style="color:red; font-size: 16px">'.$username.'</span></strong><hr></div>';
+	$html[] = '			<div class="col-xs-12" style="padding:0px 10px 3px 20px;"><strong>'._('Secret').' :<br> <span style="color:red; font-size: 16px">'.$User->user->{'2fa_secret'}.'</span></strong></div>';
 	$html[] = '		</div>';
 	$html[] = '		<div class="text-center">';
 	$html[] = '		<hr>'._('You can also scan following QR code with your preferred authenticator application').':<br><br>';
diff --git a/app/tools/user-menu/account.php b/app/tools/user-menu/account.php
index 8acc832577c16778cee31e761f23fa6fe4f06770..88b5a03184d7c686ef25bb8251df28826e8d6b3d 100644
--- a/app/tools/user-menu/account.php
+++ b/app/tools/user-menu/account.php
@@ -21,6 +21,14 @@ $User->check_user_session();
 
 # fetch all languages
 $langs = $User->fetch_langs();
+
+// passkeys
+if ($User->settings->{'passkeys'}=="1") {
+	// get user passkeys
+	$user_passkeys = $User->get_user_passkeys($User->user->id);
+	// set passkey_only flag
+	$passkey_only = $User->settings->{'passkeys'}=="1" && sizeof($user_passkeys)>0 && $User->user->passkey_only=="1" ? true : false;
+}
 ?>
 
 <!-- test -->
@@ -75,6 +83,21 @@ if($User->user->authMethod == 1) {
 </tr>
 <?php } ?>
 
+
+<?php if ($User->settings->{'passkeys'}=="1") { ?>
+<!-- passkey login only -->
+<tr>
+    <td><?php print _('Passkey login only'); ?></td>
+    <td>
+		<input type="checkbox" value="1" class="input-switch" name="passkey_only" <?php if($User->user->passkey_only == "1") print 'checked'; ?>>
+    </td>
+    <td class="info2"><?php print _('Select to only allow account login with passkey'); ?>
+    	<?php if(sizeof($user_passkeys)==0 && $User->user->passkey_only=="1") { print "<br><span class='text-warning'>". _("You can login to your account with normal authentication method only untill you create passkeys.")."</span>"; } ?>
+    </td>
+</tr>
+<?php } ?>
+
+
 <!-- select theme -->
 <tr>
 	<td><?php print _('Theme'); ?></td>
diff --git a/app/tools/user-menu/index.php b/app/tools/user-menu/index.php
index 7c092a79ecaa5d92b6e6c644979bbaab83473da0..f245bb8a5c818fa3c13f1b92f4afcde3f30ffd9d 100755
--- a/app/tools/user-menu/index.php
+++ b/app/tools/user-menu/index.php
@@ -35,6 +35,11 @@ print "<hr><br>";
 	$subpages['2fa'] = "Two-factor authentication";
 	}
 
+	// Passkeys
+	if ($User->settings->{'passkeys'}=="1") {
+	$subpages['passkeys'] = "Passwordless authentication";
+	}
+
 	// default tab
 	if(!isset($_GET['subnetId'])) {
 		$_GET['subnetId'] = "account";
diff --git a/app/tools/user-menu/passkey_challenge.php b/app/tools/user-menu/passkey_challenge.php
new file mode 100644
index 0000000000000000000000000000000000000000..06113c5139b15fbf6faad2546b7c41a3c6b98364
--- /dev/null
+++ b/app/tools/user-menu/passkey_challenge.php
@@ -0,0 +1,28 @@
+<?php
+
+#
+# Create challenge for webauthn
+#
+
+// include composer
+require __DIR__ . '/../../../functions/vendor/autoload.php';
+
+// phpipam stuff
+require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
+
+# initialize required objects - to start session
+$Database       = new Database_PDO;
+$User           = new User ($Database);
+
+// webauthn modules
+use Firehed\WebAuthn\{
+    SessionChallengeManager
+};
+
+// Generate challenge
+$challengeManager = new \Firehed\WebAuthn\SessionChallengeManager();
+$challenge = $challengeManager->createChallenge();
+
+// Send json challenge to user
+header('Content-type: application/json');
+echo json_encode($challenge->getBase64());
\ No newline at end of file
diff --git a/app/tools/user-menu/passkey_edit.php b/app/tools/user-menu/passkey_edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2da207df606394cf4106eb4305b793d6d4ece6c
--- /dev/null
+++ b/app/tools/user-menu/passkey_edit.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ *
+ * Name created passkey
+ */
+
+# include required scripts
+require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
+
+# initialize user object
+$Database 	= new Database_PDO;
+$User 		= new User ($Database);
+$Result 	= new Result ();
+
+# verify that user is logged in
+$User->check_user_session();
+
+# validate action
+$User->validate_action ($_POST['action'], true);
+
+# create csrf token
+$csrf = $User->Crypto->csrf_cookie ("create", "passkeyedit");
+
+# fetch passkey
+$passkey = $User->get_user_passkey_by_keyId ($_POST['keyid']);
+
+# validate
+if(is_null($passkey))
+$Result->show("danger", _("Passkey not found"), true, true);
+?>
+
+
+<!-- header -->
+<div class="pHeader"><?php print _("Edit")." "._("passkey"); ?></div>
+
+<!-- content -->
+<div class="pContent">
+
+	<?php
+	if($_POST['action']=="add") {
+		$Result->show("success", _("New passkey succesfully registered!"));
+		print "<hr>";
+	}
+	?>
+
+	<form id="passkeyEdit" name="passkeyEdit">
+
+	<?php if ($_POST['action']!="delete") { ?>
+	<table class="groupEdit table table-noborder table-condensed">
+	<!-- name -->
+	<tr>
+	    <td><?php print _('Name your passkey'); ?>:</td>
+	    <td>
+
+	    	<input type="text" name="comment" class="form-control input-sm" value="<?php print escape_input(@$passkey->comment); ?>" <?php if($_POST['action'] == "delete") print "readonly"; ?>>
+	        <input type="hidden" name="keyid" value="<?php print escape_input($_POST['keyid']); ?>">
+    		<input type="hidden" name="action" value="<?php print escape_input($_POST['action']); ?>">
+    		<input type="hidden" name="csrf_cookie" value="<?php print $csrf; ?>">
+	    </td>
+    </tr>
+	</table>
+    <?php } else { ?>
+	        <input type="hidden" name="keyid" value="<?php print escape_input($_POST['keyid']); ?>">
+    		<input type="hidden" name="action" value="<?php print escape_input($_POST['action']); ?>">
+    		<input type="hidden" name="csrf_cookie" value="<?php print $csrf; ?>">
+    <?php } ?>
+
+    <?php
+	if($_POST['action']=="delete") {
+		$Result->show("danger", _("You are about to delete your passkey ").escape_input($passkey->comment)."!", false);
+	}
+	?>
+</form>
+
+</div>
+
+
+<!-- footer -->
+<div class="pFooter">
+	<div class="btn-group">
+		<button class="btn btn-sm btn-default hidePopups"><?php print _('Cancel'); ?></button>
+		<button class='btn btn-sm btn-default submit_popup <?php if($_POST['action']=="delete") { print "btn-danger"; } else { print "btn-success"; } ?>' data-script="app/tools/user-menu/passkey_edit_result.php" data-result_div="passkeyEditResult" data-form='passkeyEdit'>
+			<i class="fa <?php if($_POST['action']=="add") { print "fa-plus"; } else if ($_POST['action']=="delete") { print "fa-trash-o"; } else { print "fa-check"; } ?>"></i> <?php print escape_input(ucwords(_($_POST['action']))); ?>
+		</button>
+
+	</div>
+	<!-- Result -->
+	<div id="passkeyEditResult"></div>
+</div>
\ No newline at end of file
diff --git a/app/tools/user-menu/passkey_edit_result.php b/app/tools/user-menu/passkey_edit_result.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ee0b3f3699d207c717309495d9bea2bb380ca4c
--- /dev/null
+++ b/app/tools/user-menu/passkey_edit_result.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ *
+ * Rename passkey
+ *
+ */
+
+# include required scripts
+require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
+
+# initialize required objects
+$Database       = new Database_PDO;
+$Result         = new Result;
+$User           = new User ($Database);
+
+# verify that user is logged in
+$User->check_user_session();
+
+# strip input tags
+$_POST = $User->strip_input_tags($_POST);
+
+# validate csrf cookie
+$User->Crypto->csrf_cookie ("validate", "passkeyedit", $_POST['csrf_cookie']) === false ? $Result->show("danger", _("Invalid CSRF cookie"), true) : "";
+
+# fetch passkey
+$passkey = $User->get_user_passkey_by_keyId ($_POST['keyid']);
+
+# validate
+if(is_null($passkey)) {
+	$Result->show("danger", _("Passkey not found"), true);
+}
+elseif ($passkey->user_id!=$User->user->id) {
+	$Result->show("danger", _("Passkey not found"), true);
+}
+else {
+	if($_POST['action']=="edit" || $_POST['action']=="add") {
+		if($User->rename_passkey ($passkey->id, $_POST['comment'])) { $Result->show("success", _("Passkey renamed"), false); }
+		else 														{ $Result->show("success", _("Failed to rename passkey"), false); }
+	}
+	else {
+		if($User->delete_passkey ($passkey->id)) 					{ $Result->show("success", _("Passkey removed"), false); }
+		else 														{ $Result->show("success", _("Failed to remove passkey"), false); }
+	}
+}
\ No newline at end of file
diff --git a/app/tools/user-menu/passkey_save.php b/app/tools/user-menu/passkey_save.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c15ed5462e1418c00eb9cd2711079e88c44798c
--- /dev/null
+++ b/app/tools/user-menu/passkey_save.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ *
+ * Save users passkey
+ *
+ */
+
+# include composer
+require __DIR__ . '/../../../functions/vendor/autoload.php';
+
+// phpipam stuff
+require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
+
+# initialize required objects
+$Database       = new Database_PDO;
+$User           = new User ($Database);
+
+// set header typw
+header('Content-Type: text/html; charset=utf-8');
+
+// webauthn modules
+use Firehed\WebAuthn\{
+    ChallengeManagerInterface,
+    Codecs,
+    CredentialContainer,
+    RelyingParty,
+    SessionChallengeManager,
+    SingleOriginRelyingParty,
+    ResponseParser
+};
+
+# process request
+$json = file_get_contents('php://input');
+$data = json_decode($json, true);
+
+// parser
+$parser = new ResponseParser();
+$createResponse = $parser->parseCreateResponse($data);
+
+// escape keyId
+$data['keyId'] =  $User->strip_input_tags ($data['keyId']);
+
+// Relaying party
+$rp = new \Firehed\WebAuthn\SingleOriginRelyingParty('https://ipam-dc.ugbb.net');
+// challange manager
+$challengeManager = new \Firehed\WebAuthn\SessionChallengeManager();
+
+// Verify credentials, if it fails exit
+try {
+    $credential = $createResponse->verify($challengeManager, $rp);
+} catch (Throwable) {
+    header('HTTP/1.1 403 Unauthorized');
+    return;
+}
+
+// encode credentials to store to database
+$codec = new Codecs\Credential();
+$encodedCredential = $codec->encode($credential);
+
+
+// save passkey
+$User->save_passkey ($encodedCredential, $credential->getStorageId(), $data['keyId']);
+
+// print result
+header('HTTP/1.1 200 OK');
+header('Content-type: application/json');
+echo json_encode([
+        'success' => true,
+        'credentialId' => $credential->getStorageId()
+]);
\ No newline at end of file
diff --git a/app/tools/user-menu/passkeys.php b/app/tools/user-menu/passkeys.php
new file mode 100644
index 0000000000000000000000000000000000000000..82709412b024c7883180748f0acd13f850bba3de
--- /dev/null
+++ b/app/tools/user-menu/passkeys.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * Usermenu - passkeys
+ */
+
+# verify that user is logged in
+$User->check_user_session();
+?>
+
+
+<h4><?php print _('Passwordless authentication'); ?></h4>
+<hr>
+<span class="info2"><?php print _("Here you can manage passkey authentication for your account"); ?>. <?php print _("Passkeys are a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN"); ?>.</span>
+<br><br>
+
+<?php
+
+# tls check
+if (!$Tools->isHttps()) {
+	$Result->show("danger", _("TLS is required for passcode authentication"), false);
+}
+# are passkeys enabled ?
+elseif (!$User->settings->{'passkeys'}=="1") {
+	$Result->show("danger", _("Passkey authentication is disabled"), false);
+}
+else {
+	// get user passkeys
+	$user_passkeys = $User->get_user_passkeys(false);
+
+
+	# passkey 0nly ?
+	if(sizeof($user_passkeys)>0 && $User->user->passkey_only=="1") {
+		$Result->show("warning alert-absolute", _("You can login to your account with with passkeys only").".<hr>"._("This can be changed under Account details tab").".");
+	}
+	elseif($User->user->passkey_only=="1") {
+		$Result->show("warning alert-absolute", _("You can login to your account with normal authentication method only untill you create passkeys".".<hr>"._("This can be changed under Account details tab.")));
+	}
+	else {
+		$Result->show("warning alert-absolute", _("You can login to your account with normal authentication method or with passkeys".".<hr>"._("This can be changed under Account details tab.")));
+	}
+	print "<div class='clearfix'></div>";
+
+	// none ?
+	if (sizeof($user_passkeys)>0) {
+		print '<div class="panel panel-default" style="max-width:600px">';
+		print '<div class="panel-heading">'._("Your passkeys").'</div>';
+		print '	<ul class="list-group">';
+
+		foreach ($user_passkeys as $passkey) {
+
+			// format last used and created
+			$created          = date("M d, Y", strtotime($passkey->created));
+			$last_used        = is_null($passkey->used) ? _("Never") : date("M d, Y", strtotime($passkey->used));
+			$passkey->comment = is_null($passkey->comment) ? "-- Unknown --" : $passkey->comment;
+			$this_browser	  = $passkey->keyId == @$_SESSION['keyId'] ? "<span class='badge' style='margin-bottom:2px;margin-left:10px;'>"._("You authenticated with this passkey")."</span>" : "";
+
+			print '<li class="list-group-item">';
+			print "<div>";
+			print '	<div style="width:40px;float:left" class="text-muted">';
+			print '	<span class="float-left text-center text-muted">
+            			<svg height="40" aria-hidden="true" viewBox="0 -8 32 32" version="1.1" width="40" data-view-component="true" class="octicon octicon-passkey-fill" style="color:red !important;">
+    						<path d="M9.496 2a5.25 5.25 0 0 0-2.519 9.857A9.006 9.006 0 0 0 .5 20.228a.751.751 0 0 0 .728.772h5.257c3.338.001 6.677.002 10.015 0a.5.5 0 0 0 .5-.5v-4.669a.95.95 0 0 0-.171-.551 9.02 9.02 0 0 0-4.814-3.423A5.25 5.25 0 0 0 9.496 2Z"></path>
+    						<path d="M23.625 10.313c0 1.31-.672 2.464-1.691 3.134a.398.398 0 0 0-.184.33v.886a.372.372 0 0 1-.11.265l-.534.534a.188.188 0 0 0 0 .265l.534.534c.071.07.11.166.11.265v.347a.374.374 0 0 1-.11.265l-.534.534a.188.188 0 0 0 0 .265l.534.534a.37.37 0 0 1 .11.265v.431a.379.379 0 0 1-.097.253l-1.2 1.319a.781.781 0 0 1-1.156 0l-1.2-1.319a.379.379 0 0 1-.097-.253v-5.39a.398.398 0 0 0-.184-.33 3.75 3.75 0 1 1 5.809-3.134ZM21 9.75a1.125 1.125 0 1 0-2.25 0 1.125 1.125 0 0 0 2.25 0Z"></path>
+						</svg>';
+          	print '	</span>';
+    		print "	</div>";
+
+          	print "<div class='pull-left' style='padding-top:8px;'>";
+          	print "<strong>".$User->strip_input_tags($passkey->comment)."</strong> ".$this_browser;
+          	print "</div>";
+
+			print '	<div class="btn-group pull-right" style="padding-top:8px;">';
+			print '		<button class="btn btn-xs btn-default open_popup" data-script="app/tools/user-menu/passkey_edit.php" data-action="edit" data-keyId="'.$passkey->keyId.'" rel="tooltip" title="" data-original-title="'._("Rename").'"><i class="fa fa-pencil"></i></button>';
+			print '		<button class="btn btn-xs btn-default open_popup" data-script="app/tools/user-menu/passkey_edit.php" data-action="delete" data-keyId="'.$passkey->keyId.'" rel="tooltip" title="" data-original-title="'._("Delete").'"><i class="fa fa-times"></i></button>';
+			print '	</div>';
+
+			// print "<div class='clearfix'></div>";
+			print "<br><br>";
+          	print "<span class='text-muted' style='padding-left:0px;'>"._("Added on")." ".$created." :: "._("Last used")." $last_used</span>";
+			print '</div>';
+
+
+			print '</li>';
+		}
+		print '	</ul>';
+		print '</div>';
+	}
+	// result
+	print '<div id="loginCheckPasskeys" style="max-width:600px"></div>';
+
+	// add
+	print '<button class="btn btn-sm btn-success addPasskey"><i class="fa fa-plus"></i> '._("Add a passkey").'</button>';
+}
+?>
+
+
+<script type="text/javascript">
+
+function loginRedirect2() {
+    location.reload()
+}
+
+// register function
+const startRegister = async (e) => {
+
+	// check if browser supports webauthn
+    if (!window.PublicKeyCredential) {
+        return
+    }
+
+    try {
+    	// get and parse challenge
+		const challengeReq = await fetch('app/tools/user-menu/passkey_challenge.php')
+		const challengeB64 = await challengeReq.json()
+		const challenge    = atob(challengeB64) // base64-decode
+
+		// create
+	    const createOptions = {
+	        publicKey: {
+	            rp: {
+	                name: 'https://ipam-dc.ugbb.net',
+	            },
+	            user: {
+	                name: "<?php print $User->user->username; ?>",
+	                displayName: "<?php print $User->user->real_name; ?>",
+	                id: Uint8Array.from("<?php print $User->user->id; ?>", c => c.charCodeAt(0)),
+	            },
+	            // This base64-decodes the response and translates it into the Webauthn-required format.
+	            challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+	            pubKeyCredParams: [
+	                {
+	                    alg: -7, // ES256
+	                    type: "public-key",
+	                },
+				    // {
+				    // 	alg: -257, // Value registered by this specification for "RS256"
+				    // 	type: "public-key",
+				    // }
+	            ]
+	        },
+	        attestation: 'direct',
+	    }
+
+	    // Call the WebAuthn browser API and get the response. This may throw, which you
+	    // should handle. Example: user cancels or never interacts with the device.
+	    const credential = await navigator.credentials.create(createOptions)
+        // console.log(credential)
+
+	    // Format the credential to send to the server. This must match the format
+	    // handed by the ResponseParser class. The formatting code below can be used
+	    // without modification.
+	    const dataForResponseParser = {
+	        rawId: Array.from(new Uint8Array(credential.rawId)),
+	        keyId: credential.id,
+	        type: credential.type,
+	        attestationObject: Array.from(new Uint8Array(credential.response.attestationObject)),
+	        clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
+	        transports: credential.response.getTransports(),
+	    }
+
+	    // Send this to your endpoint - adjust to your needs.
+	    const request = new Request('app/tools/user-menu/passkey_save.php', {
+	        body: JSON.stringify(dataForResponseParser),
+	        headers: {
+	            'Content-type': 'application/json',
+	        },
+	        method: 'POST',
+	    })
+	    const result = await fetch(request)
+
+	    // process result
+        if(result.status==200) {
+            // $('#loginCheckPasskeys').html("<div class='alert alert-success'>New passkey registered!</div>");
+
+            // open popup to name passkey
+		    $('div.loading').show();
+		    // post
+		    $.post("/app/tools/user-menu/passkey_edit.php", {"keyid":credential.id, "action":"add"}, function(data) {
+		        // set content
+		        $('#popupOverlay .popup_w500').html(data).show();
+		        // show overlay
+		        $("#popupOverlay").fadeIn('fast');
+		        $('#popupOverlay2 > div').empty();
+		        $('div.loading').hide();
+    			//disable page scrolling on bottom
+    			$('body').addClass('stop-scrolling');
+    			// reset size
+        		var myheight = $(window).height() - 250;
+        		$(".popup .pContent").css('max-height', myheight);
+
+		    }).fail(function(jqxhr, textStatus, errorThrown) {
+
+		    	$('div.jqueryError').fadeIn('fast');
+    			$('.jqueryErrorText').html(jqxhr.statusText+"<br>Status: "+textStatus+"<br>Error: "+errorThrown).show();
+    			$('div.loading').hide();
+    		});
+
+			/* this functions saves popup result */
+			/* --------------------------------- */
+			// function submit_popup_data (result_div, target_script, post_data, reload) {
+			//     // show spinner
+			//     showSpinner();
+			//     // set reload
+			//     reload = typeof reload !== 'undefined' ? reload : true;
+			//     // post
+			//     $.post(target_script, post_data, function(data) {
+			//         $('div'+result_div).html(data).slideDown('fast');
+			//         //reload after 2 seconds if succeeded!
+			//         if(reload) {
+			//             if(data.search("alert-danger")==-1 && data.search("error")==-1 && data.search("alert-warning")==-1 )    { setTimeout(function (){window.location.reload();}, 1500); }
+			//             else                                                                                                    { hideSpinner(); }
+			//         }
+			//         else {
+			//             hideSpinner();
+			//         }
+			//     }).fail(function(jqxhr, textStatus, errorThrown) { showError(jqxhr.statusText + "<br>Status: " + textStatus + "<br>Error: "+errorThrown); });
+			//     // prevent reload
+			//     return false;
+			// }
+
+
+        }
+        else {
+            $('#loginCheckPasskeys').html("<div class='alert alert-danger'>Failed to register new passkey.</div>");
+            console.log(result)
+            $('div.loading').hide();
+        }
+	}
+	catch(err) {
+		$('#loginCheckPasskeys').html("<div class='alert alert-danger'>Failed to register new passkey.</div>");
+		console.log(err);
+	}
+}
+
+
+// Start registration of new passkey
+$(document).ready(function() {
+	// check if browser supports webauthn and disable add passkey button
+    if (!window.PublicKeyCredential) {
+        $('.addPasskey').addClass('disabled').removeClass('addPasskey')
+    }
+	// add passkey
+	$('.addPasskey').click(function () {
+		startRegister ()
+		return false;
+	})
+
+})
+
+
+</script>
\ No newline at end of file
diff --git a/app/tools/user-menu/user-edit.php b/app/tools/user-menu/user-edit.php
index 915ea69fba730cf80569208af825f29650306f4c..edbeb1fc4602483327d888441a59abe53f11d355 100755
--- a/app/tools/user-menu/user-edit.php
+++ b/app/tools/user-menu/user-edit.php
@@ -43,6 +43,26 @@ if (!empty($_POST['theme'])) {
 	if (!in_array($_POST['theme'], ['default', 'white', 'dark'])) 				{ $Result->show("danger alert-absolute", _('Invalid theme'), true); }
 }
 
+# passkeys
+if ($User->settings->{'passkeys'}=="1") {
+	// fetch passkeys
+	$user_passkeys = $User->get_user_passkeys($User->user->id);
+	// check
+	if(isset($_POST['passkey_only'])) {
+		if(sizeof($user_passkeys)==0) {
+			$Result->show("warning alert-absolute", _('There are no passkeys set for user. Resetting passkey login only to false.'), false);
+			print "<div class='clearfix'></div>";
+			$_POST['passkey_only'] = 0;
+		}
+		else {
+			$_POST['passkey_only'] = 1;
+		}
+	}
+	else {
+		$_POST['passkey_only'] = 0;
+	}
+}
+
 # set override
 $_POST['compressOverride'] = @$_POST['compressOverride']=="Uncompress" ? "Uncompress" : "default";
 
diff --git a/css/bootstrap/bootstrap-custom-dark.css b/css/bootstrap/bootstrap-custom-dark.css
index 463b68979ae6d2418098cf01377119535ebfd656..b1c77434690fffd2d111823717cc6d2482fe4050 100644
--- a/css/bootstrap/bootstrap-custom-dark.css
+++ b/css/bootstrap/bootstrap-custom-dark.css
@@ -1,2 +1,1017 @@
-html{min-height:100% !important}html body{background:url("../images/bg-light.png") no-repeat center center fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;background-repeat:repeat;height:100% !important;min-height:100% !important;color:#e5e5e5;background-color:transparent}html #header{background:rgba(0,0,0,0.4)}html .hero-unit a{text-shadow:none}html .hero-unit a:hover{color:white;text-decoration:none}html blockquote{border-left-color:#58606b}html .content_overlay{padding-bottom:50px}html h1,html h2,html h3,html h4,html h5{color:white !important;text-shadow:none}html a,html a:hover{color:#58ACFA}html .wrapper{background:transparent url("../images/noise.png");height:100% !important}html .subtitle{background:rgba(0,0,0,0.2);border-bottom:1px solid rgba(0,0,0,0.4)}html pre{background:rgba(0,0,0,0.1);border:1px solid #58606b;color:#ccc}html .text-muted{color:#999}html hr{border-top:none;border-bottom:1px solid rgba(255,255,255,0.1)}html hr.title{border-top:none;border-bottom:none;margin-bottom:10px}html .alert.alert-info{background:rgba(0,0,0,0.2);border-color:#58ACFA;color:#58ACFA}html .alert.alert-warning{background:rgba(0,0,0,0.2);color:#faebcc;border-color:#8a6d3b}html .alert.alert-success{background:rgba(0,0,0,0.2);color:#d6e9c6;border-color:#d6e9c6}html .alert.alert-danger{background:rgba(0,0,0,0.2);color:#f2dede;border-color:#a94442}html .alert.alert-muted{background:transparent;color:#999}html span.text-success,html span.text-danger{border:1px solid #58606b;background:rgba(0,0,0,0.2);padding:2px 5px;border-radius:3px}html span.text-success{color:#d6e9c6;border:1px solid #d6e9c6}html span.text-danger{color:#ebccd1;border:1px solid #a94442}html table td.info2,html .info2,html .muted{text-shadow:none;color:#999}html .form-control{border:1px solid #58606b}html .form-inline{border-bottom:1px solid #58606b}html input{color:#58606b !important}html .badge{background:rgba(0,0,0,0.2) !important;border:1px solid #58606b !important}html .badge.alert-success{color:#dff0d8 !important;border-color:#3c763d !important}html .badge.alert-danger{background:rgba(0,0,0,0.2);color:#ebccd1 !important;border-color:#a94442 !important}html .badge.alert-warning{background:rgba(0,0,0,0.2);color:#faebcc !important;border-color:#8a6d3b !important}html .badge.alert-info{background:rgba(0,0,0,0.2);color:#e3eaf2 !important;border-color:#436587 !important}html span.status{border-color:rgba(0,0,0,0.6)}html span.status.status-neutral{background:rgba(0,0,0,0.1)}html span.status.status-error{background:#a94442}html span.status.status-success{background:#3c763d}html i.fa-Offline{color:#a94442 !important}html .navbar .navbar-nav.sections li.active a{background:rgba(0,0,0,0.4) !important}html .navbar .navbar-nav.sections li.dropdown ul.dropdown-menu li:hover active{background:rgba(0,0,0,0.4) !important}html .navbar .navbar-nav li{min-width:10px}html .navbar .navbar-nav li a{color:white}html .navbar a span.badge{margin-left:7px;padding:2px 5px;color:#58606b}html .navbar.navbar-default{border-top:1px solid rgba(255,255,255,0.3) !important;border-bottom:1px solid rgba(255,255,255,0.3) !important}html .navbar .navbar-nav li{min-width:10px}html .navbar#menu-navbar{background:rgba(0,0,0,0.3)}html .navbar#menu-navbar a{font-weight:normal}html .dropdown-menu .divider{background-color:#333 !important}html ul.dropdown-menu{background:url("../images/bg-light.png") no-repeat center center fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;background-repeat:repeat}html ul.dropdown-menu li.disabled{background:rgba(0,0,0,0.1);border-color:#58606b}html .navbar#menu-navbar .navbar-nav li.administration a{background:#a94442 !important}html .navbar#menu-navbar .navbar-nav li.administration li a{background:#40454a !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu{border:none;border-radius:none;box-shadow:none;padding-top:0px !important;margin-top:5px}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu li{background:transparent url("../images/noise.png") !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu li a{border-left:1px solid #58606b;border-left:none}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li{border-left:1px solid #58606b}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li:first-child{border-top:1px solid #58606b !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li:last-child{border-bottom:1px solid #58606b !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.active{border-left:2px solid #d43f3a !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.active a{background:#272b30 !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.nav-header{background:rgba(0,0,0,0.2) !important;border-bottom:1px solid #58606b}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.nav-header:hover{background:rgba(0,0,0,0.2) !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li{border-radius:0px !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li a{border-radius:0px !important}html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li.active{border-left:2px solid #58ACFA !important}html .nav-tabs{border-bottom:1px solid #999}html .nav-tabs>li.active>a,html .nav-tabs>li.active>a:focus,html .nav-tabs>li.active>a:hover{background:rgba(0,0,0,0.3);border:1px solid #999 !important;border-bottom:none !important;color:white !important}html .nav-tabs>li>a:hover{background:rgba(0,0,0,0.1);border:1px solid transparent !important;border-bottom:0px solid #999 !important;color:white !important}html .nav-tabs li a{border-bottom:none !important}html .btn:hover i.prefix{color:white}html ul.dropdown-menu{border:1px solid rgba(0,0,0,0.5)}html ul.dropdown-menu li a{color:white !important}html ul.dropdown-menu li a:hover{background:rgba(0,0,0,0.2)}html .btn{color:#58ACFA;border:1px solid #58606b}html .btn:hover{background:#58ACFA;color:white}html .btn[disabled]:hover{color:#58ACFA;border-color:#58ACFA}html .btn.btn-success{border:1px solid transparent !important;background:rgba(0,255,0,0.2) !important}html .btn.btn-success:hover{background:#3c763d;color:white;border-color:#adadad !important}html .btn{background:rgba(0,0,0,0.2);color:white}html .btn:hover{background:rgba(0,0,0,0.4)}html .input-group-addon{background:rgba(0,0,0,0.2);color:white;border:1px solid #58606b}html .breadcrumb .active{color:#999}html div.btn-group{border:1px solid #58606b;border:none;border-radius:4px}html div.btn-group.noborder{border:none}html div.btn-group.noborder .btn-group{border:none}html div.btn-group .btn.btn-danger{color:#fff;background-color:#d9534f !important;border-color:#d43f3a}html .btn-default.active,html .btn-default:active,html .open>.dropdown-toggle.btn-default{background:rgba(0,0,0,0.2) !important;color:white !important}html .btn:focus{background-position:0 -14px}html ul#subnets li.folder i,html ul.submenu li i.fa-folder-close-o,html ul.submenu li i.fa-folder-open-o,html ul.submenu li i.fa-folder{background:transparent;color:white !important}html .fa-gray{color:white !important}html .fa-folder-open{color:white !important}html .action{text-shadow:none;background:transparent;border-top:1px solid #58606b;border-bottom:1px solid #58606b}html .action .btn-success{background:#3c763d;border:1px solid #58606b !important}html .tooltip{font-size:12px}html .popover{background:url("../images/bg-light.png") no-repeat center center fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;background-repeat:repeat;color:#e5e5e5;background-color:transparent;border:2px solid rgba(0,0,0,0.2)}html .popover .popover-title{background:rgba(0,0,0,0.3);border-bottom:1px solid #999}html .popover .popover-content table{background:transparent !important}html .popover.right>.arrow:after{border-right-color:rgba(0,0,0,0.1) !important}html .table.ipaddresses tbody tr:hover td table.popover_table tbody td{background:transparent !important;border:none !important}html table#userModSelf.table td{border:none !important}html table.table-certificates tr.warning td{color:#FFC47B !important}html table.table-certificates tr.warning td a{color:#FFC47B !important}html table.table-certificates tr.danger td{color:#EA6C85 !important}html table.table-certificates tr.danger td a{color:#EA6C85 !important}html .legend .legendLabel{color:#999}html ul[class*="submenu-"] li{padding-left:20px !important;background:url("../images/li-dark.png") no-repeat 4px -3px}html ul[class*="submenu-"] li:last-child{background:url("../images/ul-li-bg-dark.png") no-repeat 3px 0px}html li.active ul[class*="submenu-"] li:last-child{background:url("../images/ul-li-bg-active-dark.png") no-repeat 3px 0px !important}html ul#subnets li.folder li.leaf.active{background:#F1FAFE url("../images/li-dark.png") no-repeat 4px -3px !important}html ul[class*="submenu-"] li.folder.active{background:#F1FAFE url("../images/ul-li-bg-dark.png") no-repeat 3px 0px !important}html ul#subnets li.folder li.leaf.active:last-of-type{background:#F1FAFE url("../images/ul-li-bg-active-dark.png") no-repeat 3px 0px !important}html table#manageSubnets tr td:nth-child(1) a{color:white}html table#manageSubnets .structure{background:url("../images/sn-bg-dark.png") 0px 7px}html table#manageSubnets .structure-last{background:url("../images/sn-bg-last-dark.png") no-repeat 1px 0px}html ul.submenu-dns li{background:url("../images/li-dns-dark.png") no-repeat 4px -3px !important;font-size:10px}html ul.submenu-dns li:last-child{background:url("../images/li-dns-last-dark.png") no-repeat 4px -3px !important}html table#manageSubnets>tbody>tr:last-child .structure{background:url("../images/sn-bg-last-dark.png") no-repeat 1px 0px !important}html ul.submenu-linked li{background:url("../images/li-dns-dark.png") no-repeat 4px -1px !important}html ul.submenu-linked li:last-child{background:url("../images/li-dns-last-dark.png") no-repeat 4px -1px !important}html table tr.similar td:nth-child(1){background:url("../images/li-dns-dark.png") no-repeat 17px -5px !important}html table tr.similar-last td:nth-child(1){background:url("../images/li-dns-last-dark.png") no-repeat 17px -5px !important}html table tr.similar-last td:nth-child(1){background:url("../images/li-dns-last-dark.png") no-repeat 17px -5px !important}html .ipaddress_subnet .subnet_badge{background:rgba(0,0,0,0.3) !important}html .table.ipaddresses tbody tr:hover td,html .table.slaves tbody tr:hover td,html .table-striped tbody tr:hover td{background:rgba(0,0,0,0.1) !important}html .table-striped tbody tr:nth-child(even) td.th{background:transparent !important}html table.table.ipaddresses .unused{text-shadow:none !important;color:white;background:rgba(0,255,0,0.1) !important}html table.table.ipaddresses tr.dhcp td{text-shadow:none !important;color:white;background:rgba(0,0,0,0.1) !important}html table.table.ipaddresses tr:hover td.unused{background:rgba(0,255,0,0.1) !important}html table.table.ipaddresses td{border-bottom:none !important;border-top:1px solid #58606b !important}html table.table.address_details td{border:none !important}html table#logs tr td.severity span{color:#272b30}html table#logs tr.success td a{border:none}html table#logs tr.danger td{background:#a94442 !important}html table.table-threshold td{border-bottom:none !important}html .ip_vis span{background:rgba(0,0,0,0.2) !important}html .ip_vis span.ip-unused{border-color:#999;color:#ccc !important}html .ip_vis span.ip-0{color:white !important}html .ip_vis span.ip-1{border-color:#a94442;background-color:rgba(255,0,0,0.05) !important;color:white !important}html .ip_vis span.ip-2{border-color:#d6e9c6;background:rgba(0,255,0,0.05) !important;color:white !important}html .ip_vis span.ip-4{border-color:#58ACFA;color:#e3eaf2 !important}html #popupOverlay{background:rgba(0,0,0,0.6) !important}html #popup{background:url("../images/bg-light.png") no-repeat center center fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;background-repeat:repeat;color:#e5e5e5;background-color:transparent;border:2px solid rgba(0,0,0,0.2)}html #popup div.pHeader{background:rgba(0,0,0,0.3);border-bottom:1px solid #999}html #popup div.pContent{background:transparent url("../images/noise.png")}html #popup div.pFooter{background:rgba(0,0,0,0.3);border-top:1px solid #999 !important;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}html #popup div.pFooter .btn{border:1px solid #58606b !important}html #popup div.col-xs-12.col-md-6{border-right-color:#58606b !important}html #popup .sortable li{background:rgba(0,0,0,0.2);border:1px solid #58606b !important}html .bootstrap-switch{border-color:#999}html .bootstrap-switch .bootstrap-switch-label{background:transparent}html .bootstrap-switch .bootstrap-switch-handle-off,html .bootstrap-switch .bootstrap-switch-handle-on{color:white !important;background:rgba(0,0,0,0.2) !important}html table.table.table-top th{background:white;margin:0px}html table.table.table{background:transparent}html table.table.table-noborder th,html table.table.table-noborder td{border:none}html table.table.statistics td{border:none !important;padding-top:2px;padding-bottom:2px}html table.table td{font-size:13px}html table.table td.th{padding-top:25px !important;border-bottom:1px solid #999 !important}html table.table td.border-bottom{border-bottom:1px solid #999 !important}html table.table tr.success td{text-shadow:none !important;color:white;background:rgba(0,255,0,0.1) !important;border-bottom:none !important;border-bottom:none !important}html table.table tr.success td btn,html table.table tr.success td a{border:1px solid #999}html table.table tr.success:hover td{background:rgba(0,255,0,0.1) !important}html table.vlans tr td{border:none !important}html table.vlans tr.change td{border-top:1px solid #58606b !important}html .bootstrap-table .table{border-bottom:1px solid #58606b}html table.table th,html table.table tr,html table.table td{background:transparent !important}html table.table td.ip a,html table.table td.ipaddress a{color:white !important}html table.table th{background:rgba(0,0,0,0.2) !important;border-bottom:1px solid #272b30 !important;border-top:none !important}html table.table th i.fa{background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.4)}html table.table tr:hover td{background:rgba(0,0,0,0.1) !important}html table.table tr.weeknumber th{border-top:none}html table.table tr.today{background:rgba(88,172,250,0.1) !important}html table.table tr.pw-status-V_teku{background:#3c763d !important}html table.table tr.pw-status-V_teku a{color:white}html table.table tr.pw-status-V_teku span.badge-warning{border:1px solid white;color:white !important}html table.table td{border-top:1px solid #58606b !important}html table.table td.izpad{color:white}html table.table.table-noborder tr th,html table.table.table-noborder tr td{background:transparent !important;border:none !important}html input,html textarea,html select{background:rgba(0,0,0,0.2) !important;color:white !important}html input.btn.btn-success,html textarea.btn.btn-success,html select.btn.btn-success{border:1px solid #58606b !important}html select{background:rgba(255,255,255,0.2) !important}html input[type=submit]{background:rgba(0,0,0,0.2) !important}html select{line-height:24px}html select optgroup{color:#58606b}html select option{color:#999}html table#subnetsMenu td#subnetsLeft{border-right:1px solid #58606b}html table#subnetsMenu td#subnetsLeft #leftMenu{margin-top:0px}html table#subnetsMenu td#subnetsLeft h4{padding:10px;margin:0px;background:rgba(0,0,0,0.3);border-bottom:1px solid #58606b}html table#subnetsMenu td#subnetsLeft hr{display:none}html table#subnetsMenu td#subnetsLeft a{color:white}html table#subnetsMenu td#subnetsLeft li.leaf i{color:#999 !important}html table#subnetsMenu td#subnetsLeft li.active{background-color:rgba(0,0,0,0.4) !important;background-color:#4b7387 !important;text-shadow:none;border-top:1px solid #58606b;border-bottom:1px solid #58606b}html table#subnetsMenu td#subnetsLeft li.active i{background:transparent}html .adminMenu,html .toolsMenu{background:transparent !important}html .adminMenu .panel-heading,html .toolsMenu .panel-heading{background:transparent !important;border-bottom:1px solid rgba(0,0,0,0.5)}html .adminMenu ul.list-group,html .toolsMenu ul.list-group{background:transparent !important}html .adminMenu ul.list-group li,html .toolsMenu ul.list-group li{background:rgba(0,0,0,0.2) !important;border-left-color:#58606b !important}html .adminMenu ul.list-group li.list-group-item,html .toolsMenu ul.list-group li.list-group-item{border:none}html .adminMenu ul.list-group li.active,html .toolsMenu ul.list-group li.active{background:rgba(0,0,0,0.6) !important;border:1px solid #58606b}html .adminMenu ul.list-group li:hover,html .toolsMenu ul.list-group li:hover{background:rgba(0,0,0,0.3) !important}html #dashboard .inner{background:rgba(0,0,0,0.2) !important;border:1px solid #58606b}html #dashboard .inner h4{background:rgba(0,0,0,0.35) !important;text-shadow:none}html #dashboard .inner .hContent{border-top:1px solid #58606b !important}html #dashboard .inner .hContent .icon{border-right:1px solid #58606b !important}html #dashboard .widget-dash .inner{box-shadow:none !important}html #dashboard #w-access_logs a,html #dashboard #w-error_logs a{color:white}html #dashboard span.severity0{border:1px solid #58606b;background:rgba(0,0,0,0.2);padding:2px 5px;border-radius:4px}html #dashboard span.severity1{border:1px solid #58606b;background:rgba(0,0,0,0.2);padding:2px 5px;border-radius:4px}html #dashboard span.severity2{border:1px solid #58606b;background:rgba(0,0,0,0.2);padding:2px 5px;border-radius:4px}html .menu-tools #dashboard .inner .hContent,html .menu-admin #dashboard .inner .hContent{border-top:none !important}html .adminMenu ul.list-group li.active{border-left-color:#a94442 !important}html .menu-tools ul.list-group li.active{border-left-color:#58ACFA !important}html .footer{border-top:1px solid #58606b}html .pagination ul li a{background:transparent;border:1px solid #999}html .pagination ul li a:hover{background:rgba(0,0,0,0.2);border:1px solid #999}html .pagination ul li.active a,html .pagination ul li.active a:hover{background:rgba(0,0,0,0.5)}html .res_val{background:rgba(0,0,0,0.2) !important}html .ui-slider-handle,html .slider-handle{background:#58606b !important}html .ui-slider,html .slider-track,html .slider-selection{background:#999 !important}html .progress{background:rgba(0,0,0,0.1) !important}html .progress .progress-limit-negative{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}html .progress .progress-bar-info{background:rgba(0,255,0,0.15);border-color:rgba(0,0,0,0.1) !important;color:white}html table#switchMainTable tr.switch-title,html table#vrf tr.vrf-title,html table#subnets tr.subnets-title,html table#settings tr.settings-title,html table#settings{background:transparent !important}html table#switchMainTable tr.switch-title th,html table#vrf tr.vrf-title th,html table#subnets tr.subnets-title th,html table#settings tr.settings-title th,html table#settings th{background:transparent !important;border-bottom:none !important}html table#switchMainTable tr.switch-title th h4,html table#vrf tr.vrf-title th h4,html table#subnets tr.subnets-title th h4,html table#settings tr.settings-title th h4,html table#settings th h4{background:transparent !important}html table#switchMainTable tr.switch-title td,html table#switchMainTable tr.switch-title th,html table#vrf tr.vrf-title td,html table#vrf tr.vrf-title th,html table#subnets tr.subnets-title td,html table#subnets tr.subnets-title th,html table#settings tr.settings-title td,html table#settings tr.settings-title th,html table#settings td,html table#settings th{border:none !important}html table#settings tr.settings-title{border-bottom:1px solid #58606b}html #gmap{border-color:rgba(0,0,0,0.2)}html ul#sortable{max-width:500px}html ul#sortable li{background:rgba(0,0,0,0.2);border-color:#999 !important}html .instructions{background:transparent;border:1px solid #999}html ul.icon-ul>li{border:1px solid rgba(0,0,0,0.4) !important}html .ipreqMenu{background:rgba(0,0,0,0.2);border:1px solid #58606b;right:5px}html .fixed-table-loading{background:rgba(0,0,0,0.3)}html div#login{background:rgba(0,0,0,0.2);border:1px solid #999 !important}html div#login legend{color:white}html div#login form{border-bottom:none}html div#login .iprequest{background:rgba(0,0,0,0.1);border-color:#58606b;text-shadow:none}html .install h4{border-bottom:none !important}html #searchSelect{background:#40454a !important}.subnet_map_found{border-color:#d6e9c6;background:rgba(0,255,0,0.15) !important;color:white !important}.subnet_map_found:hover{background:rgba(0,255,0,0.2) !important}.subnet_map_notfound{background-color:rgba(255,0,0,0.15) !important;color:white !important}.subnet_map_found a{color:white !important}.ip_vis_subnet span{box-shadow:0px 0px 1px #777;border:1px solid rgba(255,255,255,0.2);padding:3px;padding-top:7px;text-align:center;font-size:12px;margin-right:0px;margin-bottom:0px;width:120px;height:30px;float:left;border-left:none}.clearfix1{border-left:1px solid rgba(255,255,255,0.2)}.ip_vis_subnet span a{font-size:12px}
+html {
+  min-height: 100% !important;
+  /* active leaf */
+  /* active submenu background */
+}
+html body {
+  background: url("../images/bg-light.png") no-repeat center center fixed;
+  -webkit-background-size: cover;
+  -moz-background-size: cover;
+  -o-background-size: cover;
+  background-size: cover;
+  background-repeat: repeat;
+  height: 100% !important;
+  min-height: 100% !important;
+  color: #e5e5e5;
+  background-color: rgba(0, 0, 0, 0);
+}
+html #header {
+  background: rgba(0, 0, 0, 0.4);
+}
+html .hero-unit a {
+  text-shadow: none;
+}
+html .hero-unit a:hover {
+  color: white;
+  text-decoration: none;
+}
+html blockquote {
+  border-left-color: #58606b;
+}
+html .content_overlay {
+  padding-bottom: 50px;
+}
+html h1, html h2, html h3, html h4, html h5 {
+  color: white !important;
+  text-shadow: none;
+}
+html a, html a:hover {
+  color: #58ACFA;
+}
+html .wrapper {
+  background: transparent url("../images/noise.png");
+  height: 100% !important;
+}
+html .subtitle {
+  background: rgba(0, 0, 0, 0.2);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+}
+html pre {
+  background: rgba(0, 0, 0, 0.1);
+  border: 1px solid #58606b;
+  color: #ccc;
+}
+html .text-muted {
+  color: #999;
+}
+html hr {
+  border-top: none;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+html hr.title {
+  border-top: none;
+  border-bottom: none;
+  margin-bottom: 10px;
+}
+html .alert.alert-info {
+  background: rgba(0, 0, 0, 0.2);
+  border-color: #58ACFA;
+  color: #58ACFA;
+}
+html .alert.alert-warning {
+  background: rgba(0, 0, 0, 0.2);
+  color: #faebcc;
+  border-color: #8a6d3b;
+}
+html .alert.alert-success {
+  background: rgba(0, 0, 0, 0.2);
+  color: #d6e9c6;
+  border-color: #d6e9c6;
+}
+html .alert.alert-danger {
+  background: rgba(0, 0, 0, 0.2);
+  color: #f2dede;
+  border-color: #a94442;
+}
+html .alert.alert-muted {
+  background: transparent;
+  color: #999;
+}
+html span.text-success, html span.text-danger {
+  border: 1px solid #58606b;
+  background: rgba(0, 0, 0, 0.2);
+  padding: 2px 5px;
+  border-radius: 3px;
+}
+html span.text-success {
+  color: #d6e9c6;
+  border: 1px solid #d6e9c6;
+}
+html span.text-danger {
+  color: #ebccd1;
+  border: 1px solid #a94442;
+}
+html table td.info2,
+html .info2,
+html .muted {
+  text-shadow: none;
+  color: #999;
+}
+html .form-control {
+  border: 1px solid #58606b;
+}
+html .form-inline {
+  border-bottom: 1px solid #58606b;
+}
+html input {
+  color: #58606b !important;
+}
+html .badge {
+  background: rgba(0, 0, 0, 0.2) !important;
+  border: 1px solid #58606b !important;
+}
+html .badge.alert-success {
+  color: #dff0d8 !important;
+  border-color: #3c763d !important;
+}
+html .badge.alert-danger {
+  background: rgba(0, 0, 0, 0.2);
+  color: #ebccd1 !important;
+  border-color: #a94442 !important;
+}
+html .badge.alert-warning {
+  background: rgba(0, 0, 0, 0.2);
+  color: #faebcc !important;
+  border-color: #8a6d3b !important;
+}
+html .badge.alert-info {
+  background: rgba(0, 0, 0, 0.2);
+  color: #e3eaf2 !important;
+  border-color: #436587 !important;
+}
+html span.status {
+  border-color: rgba(0, 0, 0, 0.6);
+}
+html span.status.status-neutral {
+  background: rgba(0, 0, 0, 0.1);
+}
+html span.status.status-error {
+  background: #a94442;
+}
+html span.status.status-success {
+  background: #3c763d;
+}
+html i.fa-Offline {
+  color: #a94442 !important;
+}
+html .navbar .navbar-nav.sections li.active a {
+  background: rgba(0, 0, 0, 0.4) !important;
+}
+html .navbar .navbar-nav.sections li.dropdown ul.dropdown-menu li:hover active {
+  background: rgba(0, 0, 0, 0.4) !important;
+}
+html .navbar .navbar-nav li {
+  min-width: 10px;
+}
+html .navbar .navbar-nav li a {
+  color: white;
+}
+html .navbar a span.badge {
+  margin-left: 7px;
+  padding: 2px 5px;
+  color: #58606b;
+}
+html .navbar.navbar-default {
+  border-top: 1px solid rgba(255, 255, 255, 0.3) !important;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important;
+}
+html .navbar .navbar-nav li {
+  min-width: 10px;
+}
+html .navbar#menu-navbar {
+  background: rgba(0, 0, 0, 0.3);
+}
+html .navbar#menu-navbar a {
+  font-weight: normal;
+}
+html .dropdown-menu .divider {
+  background-color: #333 !important;
+}
+html ul.dropdown-menu {
+  background: url("../images/bg-light.png") no-repeat center center fixed;
+  -webkit-background-size: cover;
+  -moz-background-size: cover;
+  -o-background-size: cover;
+  background-size: cover;
+  background-repeat: repeat;
+}
+html ul.dropdown-menu li.disabled {
+  background: rgba(0, 0, 0, 0.1);
+  border-color: #58606b;
+}
+html .navbar#menu-navbar .navbar-nav li.administration a {
+  background: #a94442 !important;
+}
+html .navbar#menu-navbar .navbar-nav li.administration li a {
+  background: #40454a !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu {
+  border: none;
+  border-radius: none;
+  box-shadow: none;
+  padding-top: 0px !important;
+  margin-top: 5px;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu li {
+  background: transparent url("../images/noise.png") !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu li a {
+  border-left: 1px solid #58606b;
+  border-left: none;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li {
+  border-left: 1px solid #58606b;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li:first-child {
+  border-top: 1px solid #58606b !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li:last-child {
+  border-bottom: 1px solid #58606b !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.active {
+  border-left: 2px solid #d43f3a !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.active a {
+  background: #272b30 !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.nav-header {
+  background: rgba(0, 0, 0, 0.2) !important;
+  border-bottom: 1px solid #58606b;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.admin li.nav-header:hover {
+  background: rgba(0, 0, 0, 0.2) !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li {
+  border-radius: 0px !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li a {
+  border-radius: 0px !important;
+}
+html .navbar#menu-navbar .navbar-nav ul.dropdown-menu.tools_dropdown li.active {
+  border-left: 2px solid #58ACFA !important;
+}
+html .nav-tabs {
+  border-bottom: 1px solid #999;
+}
+html .nav-tabs > li.active > a, html .nav-tabs > li.active > a:focus, html .nav-tabs > li.active > a:hover {
+  background: rgba(0, 0, 0, 0.3);
+  border: 1px solid #999 !important;
+  border-bottom: none !important;
+  color: white !important;
+}
+html .nav-tabs > li > a:hover {
+  background: rgba(0, 0, 0, 0.1);
+  border: 1px solid transparent !important;
+  border-bottom: 0px solid #999 !important;
+  color: white !important;
+}
+html .nav-tabs li a {
+  border-bottom: none !important;
+}
+html .btn:hover i.prefix {
+  color: white;
+}
+html ul.dropdown-menu {
+  border: 1px solid rgba(0, 0, 0, 0.5);
+}
+html ul.dropdown-menu li a {
+  color: white !important;
+}
+html ul.dropdown-menu li a:hover {
+  background: rgba(0, 0, 0, 0.2);
+}
+html .btn {
+  color: #58ACFA;
+  border: 1px solid #58606b;
+}
+html .btn:hover {
+  background: #58ACFA;
+  color: white;
+}
+html .btn[disabled]:hover {
+  color: #58ACFA;
+  border-color: #58ACFA;
+}
+html .btn.btn-success {
+  border: 1px solid transparent !important;
+  background: rgba(0, 255, 0, 0.2) !important;
+}
+html .btn.btn-success:hover {
+  background: #3c763d;
+  color: white;
+  border-color: #adadad !important;
+}
+html .btn {
+  background: rgba(0, 0, 0, 0.2);
+  color: white;
+}
+html .btn:hover {
+  background: rgba(0, 0, 0, 0.4);
+}
+html .input-group-addon {
+  background: rgba(0, 0, 0, 0.2);
+  color: white;
+  border: 1px solid #58606b;
+}
+html .breadcrumb .active {
+  color: #999;
+}
+html div.btn-group {
+  border: 1px solid #58606b;
+  border: none;
+  border-radius: 4px;
+}
+html div.btn-group.noborder {
+  border: none;
+}
+html div.btn-group.noborder .btn-group {
+  border: none;
+}
+html div.btn-group .btn.btn-danger {
+  color: #fff;
+  background-color: #d9534f !important;
+  border-color: #d43f3a;
+}
+html .btn-default.active,
+html .btn-default:active,
+html .open > .dropdown-toggle.btn-default {
+  background: rgba(0, 0, 0, 0.2) !important;
+  color: white !important;
+}
+html .btn:focus {
+  background-position: 0 -14px;
+}
+html ul#subnets li.folder i,
+html ul.submenu li i.fa-folder-close-o,
+html ul.submenu li i.fa-folder-open-o,
+html ul.submenu li i.fa-folder {
+  background: transparent;
+  color: white !important;
+}
+html .fa-gray {
+  color: white !important;
+}
+html .fa-folder-open {
+  color: white !important;
+}
+html .action {
+  text-shadow: none;
+  background: transparent;
+  border-top: 1px solid #58606b;
+  border-bottom: 1px solid #58606b;
+}
+html .action .btn-success {
+  background: #3c763d;
+  border: 1px solid #58606b !important;
+}
+html .tooltip {
+  font-size: 12px;
+}
+html .popover {
+  background: url("../images/bg-light.png") no-repeat center center fixed;
+  -webkit-background-size: cover;
+  -moz-background-size: cover;
+  -o-background-size: cover;
+  background-size: cover;
+  background-repeat: repeat;
+  color: #e5e5e5;
+  background-color: rgba(0, 0, 0, 0);
+  border: 2px solid rgba(0, 0, 0, 0.2);
+}
+html .popover .popover-title {
+  background: rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid #999;
+}
+html .popover .popover-content table {
+  background: transparent !important;
+}
+html .popover.right > .arrow:after {
+  border-right-color: rgba(0, 0, 0, 0.1) !important;
+}
+html .table.ipaddresses tbody tr:hover td table.popover_table tbody td {
+  background: transparent !important;
+  border: none !important;
+}
+html table#userModSelf.table td {
+  border: none !important;
+}
+html table.table-certificates tr.warning td {
+  color: #FFC47B !important;
+}
+html table.table-certificates tr.warning td a {
+  color: #FFC47B !important;
+}
+html table.table-certificates tr.danger td {
+  color: #EA6C85 !important;
+}
+html table.table-certificates tr.danger td a {
+  color: #EA6C85 !important;
+}
+html .legend .legendLabel {
+  color: #999;
+}
+html ul[class*=submenu-] li {
+  /* horizontal ul lines */
+  padding-left: 20px !important;
+  background: url("../images/li-dark.png") no-repeat 4px -3px;
+}
+html ul[class*=submenu-] li:last-child {
+  /* last child in inactive line  */
+  background: url("../images/ul-li-bg-dark.png") no-repeat 3px 0px;
+}
+html li.active ul[class*=submenu-] li:last-child {
+  /* last child in active line  */
+  background: url("../images/ul-li-bg-active-dark.png") no-repeat 3px 0px !important;
+}
+html ul#subnets li.folder li.leaf.active {
+  background: #F1FAFE url("../images/li-dark.png") no-repeat 4px -3px !important;
+}
+html ul[class*=submenu-] li.folder.active {
+  background: #F1FAFE url("../images/ul-li-bg-dark.png") no-repeat 3px 0px !important;
+}
+html ul#subnets li.folder li.leaf.active:last-of-type {
+  background: #F1FAFE url("../images/ul-li-bg-active-dark.png") no-repeat 3px 0px !important;
+}
+html table#manageSubnets tr td:nth-child(1) a {
+  color: white;
+}
+html table#manageSubnets .structure {
+  background: url("../images/sn-bg-dark.png") 0px 7px;
+}
+html table#manageSubnets .structure-last {
+  background: url("../images/sn-bg-last-dark.png") no-repeat 1px 0px;
+}
+html ul.submenu-dns li {
+  background: url("../images/li-dns-dark.png") no-repeat 4px -3px !important;
+  font-size: 10px;
+}
+html ul.submenu-dns li:last-child {
+  background: url("../images/li-dns-last-dark.png") no-repeat 4px -3px !important;
+}
+html table#manageSubnets > tbody > tr:last-child .structure {
+  background: url("../images/sn-bg-last-dark.png") no-repeat 1px 0px !important;
+}
+html ul.submenu-linked li {
+  background: url("../images/li-dns-dark.png") no-repeat 4px -1px !important;
+}
+html ul.submenu-linked li:last-child {
+  background: url("../images/li-dns-last-dark.png") no-repeat 4px -1px !important;
+}
+html table tr.similar td:nth-child(1) {
+  background: url("../images/li-dns-dark.png") no-repeat 17px -5px !important;
+}
+html table tr.similar-last td:nth-child(1) {
+  background: url("../images/li-dns-last-dark.png") no-repeat 17px -5px !important;
+}
+html table tr.similar-last td:nth-child(1) {
+  background: url("../images/li-dns-last-dark.png") no-repeat 17px -5px !important;
+}
+html .ipaddress_subnet .subnet_badge {
+  background: rgba(0, 0, 0, 0.3) !important;
+}
+html .table.ipaddresses tbody tr:hover td,
+html .table.slaves tbody tr:hover td,
+html .table-striped tbody tr:hover td {
+  background: rgba(0, 0, 0, 0.1) !important;
+}
+html .table-striped tbody tr:nth-child(even) td.th {
+  background: transparent !important;
+}
+html table.table.ipaddresses .unused {
+  text-shadow: none !important;
+  color: white;
+  background: rgba(0, 255, 0, 0.1) !important;
+}
+html table.table.ipaddresses tr.dhcp td {
+  text-shadow: none !important;
+  color: white;
+  background: rgba(0, 0, 0, 0.1) !important;
+}
+html table.table.ipaddresses tr:hover td.unused {
+  background: rgba(0, 255, 0, 0.1) !important;
+}
+html table.table.ipaddresses td {
+  border-bottom: none !important;
+  border-top: 1px solid #58606b !important;
+}
+html table.table.address_details td {
+  border: none !important;
+}
+html table#logs tr td.severity span {
+  color: #272b30;
+}
+html table#logs tr.success td a {
+  border: none;
+}
+html table#logs tr.danger td {
+  background: #a94442 !important;
+}
+html table.table-threshold td {
+  border-bottom: none !important;
+}
+html .ip_vis span {
+  background: rgba(0, 0, 0, 0.2) !important;
+}
+html .ip_vis span.ip-unused {
+  border-color: #999;
+  color: #ccc !important;
+}
+html .ip_vis span.ip-0 {
+  color: white !important;
+}
+html .ip_vis span.ip-1 {
+  border-color: #a94442;
+  background-color: rgba(255, 0, 0, 0.05) !important;
+  color: white !important;
+}
+html .ip_vis span.ip-2 {
+  border-color: #d6e9c6;
+  background: rgba(0, 255, 0, 0.05) !important;
+  color: white !important;
+}
+html .ip_vis span.ip-4 {
+  border-color: #58ACFA;
+  color: #e3eaf2 !important;
+}
+html #popupOverlay {
+  background: rgba(0, 0, 0, 0.6) !important;
+}
+html #popup {
+  background: url("../images/bg-light.png") no-repeat center center fixed;
+  -webkit-background-size: cover;
+  -moz-background-size: cover;
+  -o-background-size: cover;
+  background-size: cover;
+  background-repeat: repeat;
+  color: #e5e5e5;
+  background-color: rgba(0, 0, 0, 0);
+  border: 2px solid rgba(0, 0, 0, 0.2);
+}
+html #popup div.pHeader {
+  background: rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid #999;
+}
+html #popup div.pContent {
+  background: transparent url("../images/noise.png");
+}
+html #popup div.pFooter {
+  background: rgba(0, 0, 0, 0.3);
+  border-top: 1px solid #999 !important;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+html #popup div.pFooter .btn {
+  border: 1px solid #58606b !important;
+}
+html #popup div.col-xs-12.col-md-6 {
+  border-right-color: #58606b !important;
+}
+html #popup .sortable li {
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid #58606b !important;
+}
+html .bootstrap-switch {
+  border-color: #999;
+}
+html .bootstrap-switch .bootstrap-switch-label {
+  background: transparent;
+}
+html .bootstrap-switch .bootstrap-switch-handle-off,
+html .bootstrap-switch .bootstrap-switch-handle-on {
+  color: white !important;
+  background: rgba(0, 0, 0, 0.2) !important;
+}
+html table.table.table-top th {
+  background: white;
+  margin: 0px;
+}
+html table.table.table {
+  background: transparent;
+}
+html table.table.table-noborder th,
+html table.table.table-noborder td {
+  border: none;
+}
+html table.table.statistics td {
+  border: none !important;
+  padding-top: 2px;
+  padding-bottom: 2px;
+}
+html table.table td {
+  font-size: 13px;
+}
+html table.table td.th {
+  padding-top: 25px !important;
+  border-bottom: 1px solid #999 !important;
+}
+html table.table td.border-bottom {
+  border-bottom: 1px solid #999 !important;
+}
+html table.table tr.success td {
+  text-shadow: none !important;
+  color: white;
+  background: rgba(0, 255, 0, 0.1) !important;
+  border-bottom: none !important;
+  border-bottom: none !important;
+}
+html table.table tr.success td btn, html table.table tr.success td a {
+  border: 1px solid #999;
+}
+html table.table tr.success:hover td {
+  background: rgba(0, 255, 0, 0.1) !important;
+}
+html table.vlans tr td {
+  border: none !important;
+}
+html table.vlans tr.change td {
+  border-top: 1px solid #58606b !important;
+}
+html .bootstrap-table .table {
+  border-bottom: 1px solid #58606b;
+}
+html table.table th, html table.table tr, html table.table td {
+  background: transparent !important;
+}
+html table.table td.ip a,
+html table.table td.ipaddress a {
+  color: white !important;
+}
+html table.table th {
+  background: rgba(0, 0, 0, 0.2) !important;
+  border-bottom: 1px solid #272b30 !important;
+  border-top: none !important;
+}
+html table.table th i.fa {
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid rgba(255, 255, 255, 0.4);
+}
+html table.table tr:hover td {
+  background: rgba(0, 0, 0, 0.1) !important;
+}
+html table.table tr.weeknumber th {
+  border-top: none;
+}
+html table.table tr.today {
+  background: rgba(88, 172, 250, 0.1) !important;
+}
+html table.table tr.pw-status-V_teku {
+  background: #3c763d !important;
+}
+html table.table tr.pw-status-V_teku a {
+  color: white;
+}
+html table.table tr.pw-status-V_teku span.badge-warning {
+  border: 1px solid white;
+  color: white !important;
+}
+html table.table td {
+  border-top: 1px solid #58606b !important;
+}
+html table.table td.izpad {
+  color: white;
+}
+html table.table.table-noborder tr th, html table.table.table-noborder tr td {
+  background: transparent !important;
+  border: none !important;
+}
+html input,
+html textarea,
+html select {
+  background: rgba(0, 0, 0, 0.2) !important;
+  color: white !important;
+}
+html input.btn.btn-success,
+html textarea.btn.btn-success,
+html select.btn.btn-success {
+  border: 1px solid #58606b !important;
+}
+html select {
+  background: rgba(255, 255, 255, 0.2) !important;
+}
+html input[type=submit] {
+  background: rgba(0, 0, 0, 0.2) !important;
+}
+html select {
+  line-height: 24px;
+}
+html select optgroup {
+  color: #58606b;
+}
+html select option {
+  color: #999;
+}
+html table#subnetsMenu td#subnetsLeft {
+  border-right: 1px solid #58606b;
+}
+html table#subnetsMenu td#subnetsLeft #leftMenu {
+  margin-top: 0px;
+}
+html table#subnetsMenu td#subnetsLeft h4 {
+  padding: 10px;
+  margin: 0px;
+  background: rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid #58606b;
+}
+html table#subnetsMenu td#subnetsLeft hr {
+  display: none;
+}
+html table#subnetsMenu td#subnetsLeft a {
+  color: white;
+}
+html table#subnetsMenu td#subnetsLeft li.leaf i {
+  color: #999 !important;
+}
+html table#subnetsMenu td#subnetsLeft li.active {
+  background-color: rgba(0, 0, 0, 0.4) !important;
+  background-color: #4b7387 !important;
+  text-shadow: none;
+  border-top: 1px solid #58606b;
+  border-bottom: 1px solid #58606b;
+}
+html table#subnetsMenu td#subnetsLeft li.active i {
+  background: transparent;
+}
+html .adminMenu,
+html .toolsMenu {
+  background: transparent !important;
+}
+html .adminMenu .panel-heading,
+html .toolsMenu .panel-heading {
+  background: transparent !important;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.5);
+}
+html .adminMenu ul.list-group,
+html .toolsMenu ul.list-group {
+  background: transparent !important;
+}
+html .adminMenu ul.list-group li,
+html .toolsMenu ul.list-group li {
+  background: rgba(0, 0, 0, 0.2) !important;
+  border-left-color: #58606b !important;
+}
+html .adminMenu ul.list-group li.list-group-item,
+html .toolsMenu ul.list-group li.list-group-item {
+  border: none;
+}
+html .adminMenu ul.list-group li.active,
+html .toolsMenu ul.list-group li.active {
+  background: rgba(0, 0, 0, 0.6) !important;
+  border: 1px solid #58606b;
+}
+html .adminMenu ul.list-group li:hover,
+html .toolsMenu ul.list-group li:hover {
+  background: rgba(0, 0, 0, 0.3) !important;
+}
+html #dashboard .inner {
+  background: rgba(0, 0, 0, 0.2) !important;
+  border: 1px solid #58606b;
+}
+html #dashboard .inner h4 {
+  background: rgba(0, 0, 0, 0.35) !important;
+  text-shadow: none;
+}
+html #dashboard .inner .hContent {
+  border-top: 1px solid #58606b !important;
+}
+html #dashboard .inner .hContent .icon {
+  border-right: 1px solid #58606b !important;
+}
+html #dashboard .widget-dash .inner {
+  box-shadow: none !important;
+}
+html #dashboard #w-access_logs a,
+html #dashboard #w-error_logs a {
+  color: white;
+}
+html #dashboard span.severity0 {
+  border: 1px solid #58606b;
+  background: rgba(0, 0, 0, 0.2);
+  padding: 2px 5px;
+  border-radius: 4px;
+}
+html #dashboard span.severity1 {
+  border: 1px solid #58606b;
+  background: rgba(0, 0, 0, 0.2);
+  padding: 2px 5px;
+  border-radius: 4px;
+}
+html #dashboard span.severity2 {
+  border: 1px solid #58606b;
+  background: rgba(0, 0, 0, 0.2);
+  padding: 2px 5px;
+  border-radius: 4px;
+}
+html .menu-tools #dashboard .inner .hContent,
+html .menu-admin #dashboard .inner .hContent {
+  border-top: none !important;
+}
+html .adminMenu ul.list-group li.active {
+  border-left-color: #a94442 !important;
+}
+html .menu-tools ul.list-group li.active {
+  border-left-color: #58ACFA !important;
+}
+html .footer {
+  border-top: 1px solid #58606b;
+}
+html .pagination ul li a {
+  background: transparent;
+  border: 1px solid #999;
+}
+html .pagination ul li a:hover {
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid #999;
+}
+html .pagination ul li.active a,
+html .pagination ul li.active a:hover {
+  background: rgba(0, 0, 0, 0.5);
+}
+html .res_val {
+  background: rgba(0, 0, 0, 0.2) !important;
+}
+html .ui-slider-handle,
+html .slider-handle {
+  background: #58606b !important;
+}
+html .ui-slider,
+html .slider-track,
+html .slider-selection {
+  background: #999 !important;
+}
+html .progress {
+  background: rgba(0, 0, 0, 0.1) !important;
+}
+html .progress .progress-limit-negative {
+  background: transparent !important;
+  border-color: rgba(0, 0, 0, 0.1) !important;
+}
+html .progress .progress-bar-info {
+  background: rgba(0, 255, 0, 0.15);
+  border-color: rgba(0, 0, 0, 0.1) !important;
+  color: white;
+}
+html table#switchMainTable tr.switch-title,
+html table#vrf tr.vrf-title,
+html table#subnets tr.subnets-title,
+html table#settings tr.settings-title,
+html table#settings {
+  background: transparent !important;
+}
+html table#switchMainTable tr.switch-title th,
+html table#vrf tr.vrf-title th,
+html table#subnets tr.subnets-title th,
+html table#settings tr.settings-title th,
+html table#settings th {
+  background: transparent !important;
+  border-bottom: none !important;
+}
+html table#switchMainTable tr.switch-title th h4,
+html table#vrf tr.vrf-title th h4,
+html table#subnets tr.subnets-title th h4,
+html table#settings tr.settings-title th h4,
+html table#settings th h4 {
+  background: transparent !important;
+}
+html table#switchMainTable tr.switch-title td, html table#switchMainTable tr.switch-title th,
+html table#vrf tr.vrf-title td,
+html table#vrf tr.vrf-title th,
+html table#subnets tr.subnets-title td,
+html table#subnets tr.subnets-title th,
+html table#settings tr.settings-title td,
+html table#settings tr.settings-title th,
+html table#settings td,
+html table#settings th {
+  border: none !important;
+}
+html table#settings tr.settings-title {
+  border-bottom: 1px solid #58606b;
+}
+html #gmap {
+  border-color: rgba(0, 0, 0, 0.2);
+}
+html ul#sortable {
+  max-width: 500px;
+}
+html ul#sortable li {
+  background: rgba(0, 0, 0, 0.2);
+  border-color: #999 !important;
+}
+html .instructions {
+  background: transparent;
+  border: 1px solid #999;
+}
+html ul.icon-ul > li {
+  border: 1px solid rgba(0, 0, 0, 0.4) !important;
+}
+html .ipreqMenu {
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid #58606b;
+  right: 5px;
+}
+html .fixed-table-loading {
+  background: rgba(0, 0, 0, 0.3);
+}
+html div#login {
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid #999 !important;
+}
+html div#login legend {
+  color: white;
+  border-bottom: 1px solid #58606b !important;
+}
+html div#login form {
+  border-bottom: none;
+}
+html div#login .iprequest {
+  background: rgba(0, 0, 0, 0.1);
+  border-color: #58606b;
+  text-shadow: none;
+}
+html .install h4 {
+  border-bottom: none !important;
+}
+html #searchSelect {
+  background: #40454a !important;
+}
+
+.panel.panel-default {
+  background: transparent;
+  border: 1px solid rgba(255, 255, 255, 0.1) !important;
+}
+.panel.panel-default .panel-heading {
+  background: rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid rgba(255, 255, 255, 0.5) !important;
+  color: white;
+}
+.panel.panel-default .list-group-item {
+  background: transparent;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
+}
+.panel.panel-default .list-group-item:last-child {
+  border-bottom: none;
+}
+.panel.panel-default:last-child {
+  border-bottom: none !important;
+}
+
+.octicon-passkey-fill {
+  fill: #ccc;
+}
+
+.list-group-item {
+  border: none;
+}
+
+.btn-default.disabled.focus,
+.btn-default.disabled:focus,
+.btn-default.disabled:hover,
+.btn-default[disabled].focus,
+.btn-default[disabled]:focus,
+.btn-default[disabled]:hover {
+  background: rgba(0, 0, 0, 0.1) !important;
+}
+
+.subnet_map_found {
+  border-color: #d6e9c6;
+  background: rgba(0, 255, 0, 0.15) !important;
+  color: white !important;
+}
+
+.subnet_map_found:hover {
+  background: rgba(0, 255, 0, 0.2) !important;
+}
+
+.subnet_map_notfound {
+  background-color: rgba(255, 0, 0, 0.15) !important;
+  color: white !important;
+}
+
+.subnet_map_found a {
+  color: white !important;
+}
+
+.ip_vis_subnet span {
+  box-shadow: 0px 0px 1px #777;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  padding: 3px;
+  padding-top: 7px;
+  text-align: center;
+  font-size: 12px;
+  margin-right: 0px;
+  margin-bottom: 0px;
+  width: 120px;
+  height: 30px;
+  float: left;
+  border-left: none;
+}
+
+.clearfix1 {
+  border-left: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.ip_vis_subnet span a {
+  font-size: 12px;
+}
+
 /*# sourceMappingURL=bootstrap-custom-dark.css.map */
diff --git a/css/bootstrap/bootstrap-custom-dark.scss b/css/bootstrap/bootstrap-custom-dark.scss
index 5f284395e114a1c91c5d7ede1b8dd8f6137a4c19..6e04a901fa6159aedc222cd1255895f50ba16e52 100644
--- a/css/bootstrap/bootstrap-custom-dark.scss
+++ b/css/bootstrap/bootstrap-custom-dark.scss
@@ -1237,6 +1237,8 @@ html {
 
         legend {
             color: white;
+            border-bottom: 1px solid $color_gray_5 !important;
+
         }
 
         form {
@@ -1263,6 +1265,45 @@ html {
 }
 
 
+.panel.panel-default {
+    background: transparent;
+
+    border: 1px solid rgba(255,255,255,0.1) !important;
+
+    .panel-heading {
+        background: rgba(0, 0, 0, 0.3);
+        border-bottom: 1px solid rgba(255,255,255,0.5) !important;
+        color: white;
+    }
+
+    .list-group-item {
+        background: transparent;
+        border-bottom: 1px solid rgba(255,255,255,0.1) !important;
+    }
+    .list-group-item:last-child {
+        border-bottom: none;
+    }
+
+    &:last-child {
+        border-bottom: none !important;
+    }
+}
+.octicon-passkey-fill {
+  fill: #ccc;
+}
+.list-group-item {
+    border: none;
+}
+.btn-default.disabled.focus,
+.btn-default.disabled:focus,
+.btn-default.disabled:hover,
+.btn-default[disabled].focus,
+.btn-default[disabled]:focus,
+.btn-default[disabled]:hover {
+    background: rgba(0,0,0,0.1) !important;
+}
+
+
 .subnet_map_found {
     border-color: #d6e9c6;
     background: rgba(0,255,0,0.15) !important;
diff --git a/css/bootstrap/bootstrap-custom.css b/css/bootstrap/bootstrap-custom.css
index 0b7eed90574db914f3c872577af86f594527e863..a10b728f2b4df8f3d9da14537e9d1c13a613e088 100644
--- a/css/bootstrap/bootstrap-custom.css
+++ b/css/bootstrap/bootstrap-custom.css
@@ -2070,10 +2070,19 @@ div#login .login th {
 div#login .login td {
 	text-align: right;
 }
-div#loginCheck .alert {
-	margin: auto;
+div#loginCheck .alert,
+div#loginCheckPasskeys .alert {
+/* 	margin: auto; */
 	margin-top: 10px;
-	width: 500px;
+	padding: 5px;
+/* 	width: 500px; */
+}
+div#loginCheck .alert-danger:before,
+div#loginCheckPasskeys .alert-danger:before {
+   font-family: "FontAwesome";
+   content: "\f071";
+   padding-right: 8px;
+   color: #a94442;
 }
 .iprequest {
 	background: #F1FAFE;
diff --git a/db/SCHEMA.sql b/db/SCHEMA.sql
index f4cb81c84363674d5171e992b56a95b41243e62b..5b02782e3f59b04cd01cf8882dc67249cd3ec4c6 100755
--- a/db/SCHEMA.sql
+++ b/db/SCHEMA.sql
@@ -224,6 +224,7 @@ CREATE TABLE `settings` (
   `2fa_name` VARCHAR(32)  NULL  DEFAULT 'phpipam',
   `2fa_length` INT(2)  NULL  DEFAULT '16',
   `2fa_userchange` BOOL  NOT NULL  DEFAULT '1',
+  `passkeys` TINYINT(1)  NULL  DEFAULT '1',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
 /* insert default values */
@@ -370,6 +371,7 @@ CREATE TABLE `users` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `username` varchar(255) NOT NULL DEFAULT '',
   `authMethod` INT(2)  NULL  DEFAULT 1,
+  `passkey_only` TINYINT(1)  NOT NULL  DEFAULT '0',
   `password` CHAR(128) DEFAULT NULL,
   `groups` varchar(1024) DEFAULT NULL,
   `role` text,
@@ -1029,6 +1031,26 @@ CREATE TABLE `vaultItems` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
 
 
+# Dump of table passkeys
+# ------------------------------------------------------------
+DROP TABLE IF EXISTS `passkeys`;
+
+-- passkey table
+CREATE TABLE `passkeys` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `credentialId` text NOT NULL,
+  `keyId` text NOT NULL,
+  `credential` text NOT NULL,
+  `comment` text,
+  `created` timestamp NULL DEFAULT NULL,
+  `used` timestamp NULL DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
 # Dump of table nominatim
 # ------------------------------------------------------------
 DROP TABLE IF EXISTS `nominatim`;
@@ -1059,4 +1081,4 @@ CREATE TABLE `nominatim_cache` (
 # ------------------------------------------------------------
 
 UPDATE `settings` SET `version` = "1.6";
-UPDATE `settings` SET `dbversion` = 39;
+UPDATE `settings` SET `dbversion` = 40;
diff --git a/functions/PHPMailer b/functions/PHPMailer
index ee4090bd62ad3ded3eac19d6fd0213abbe3596f1..cbe9d8d9a9adb7dff77852a3cfc9b63ede3e7a89 160000
--- a/functions/PHPMailer
+++ b/functions/PHPMailer
@@ -1 +1 @@
-Subproject commit ee4090bd62ad3ded3eac19d6fd0213abbe3596f1
+Subproject commit cbe9d8d9a9adb7dff77852a3cfc9b63ede3e7a89
diff --git a/functions/classes/class.User.php b/functions/classes/class.User.php
index 263af2bde30c40232c8f65b8830b059da7082201..7a92941bd0af45a7506f75b8ac94a272bd26ed25 100644
--- a/functions/classes/class.User.php
+++ b/functions/classes/class.User.php
@@ -819,10 +819,10 @@ class User extends Common_functions {
             $this->Log->write ( _("User login"), _('Error: Invalid authentication method'), 2 );
             $this->Result->show("danger", _("Error: Invalid authentication method"), true);
         }
-        # disabled
-        elseif ($this->user->disabled=="Yes") {
-            $this->Result->show("danger", _("Your account has been disabled").".", true);
-        }
+        # disabled - we made separate check on this, therwise we reveal info before user is authenticated
+        // elseif ($this->user->disabled=="Yes") {
+        //     $this->Result->show("danger", _("Your account has been disabled").".", true);
+        // }
         else {
             # set method name variable
             $authmethodtype = $this->authmethodtype;
@@ -843,12 +843,12 @@ class User extends Common_functions {
     /**
      * tries to fetch user datails from database by username if not already existing locally
      *
-     * @access private
+     * @access public
      * @param string $username
      * @param bool $force
      * @return void
      */
-    private function fetch_user_details ($username, $force = false) {
+    public function fetch_user_details ($username, $force = false) {
         # only if not already active
         if(!is_object($this->user) || $force) {
             try {
@@ -950,6 +950,9 @@ class User extends Common_functions {
     private function auth_local ($username, $password) {
         # auth ok
         if(hash_equals($this->user->password, crypt($password, $this->user->password))) {
+            # check login restrictions for authenticated user
+            $this->check_login_restrictions ($username);
+
             # save to session
             $this->write_session_parameters ();
 
@@ -986,6 +989,9 @@ class User extends Common_functions {
      * @return void
      */
     public function auth_http ($username, $password) {
+        # check login restrictions for authenticated user
+        $this->check_login_restrictions ($username);
+
         # save to session
         $this->write_session_parameters ();
 
@@ -1080,6 +1086,9 @@ class User extends Common_functions {
         # authenticate
         try {
             if ($adldap->authenticate($username, $password)) {
+                # check login restrictions for authenticated user
+                $this->check_login_restrictions ($username);
+
                 # save to session
                 $this->write_session_parameters();
 
@@ -1193,6 +1202,8 @@ class User extends Common_functions {
 
         # authenticate user
         if($auth) {
+            # check login restrictions for authenticated user
+            $this->check_login_restrictions ($username);
             # save to session
             $this->write_session_parameters ();
 
@@ -1222,6 +1233,9 @@ class User extends Common_functions {
      * @return void
      */
     private function auth_SAML2 ($username, $password = null) {
+        # check login restrictions for authenticated user
+        $this->check_login_restrictions ($username);
+
         # save to session
         $this->write_session_parameters ();
 
@@ -1234,6 +1248,246 @@ class User extends Common_functions {
         $this->block_remove_entry ();
     }
 
+    /**
+     * Check for any login restrictions after user has authenticated
+     * @method check_login_restrictions
+     * @param  string $username
+     * @return void
+     */
+    private function check_login_restrictions ($username = "") {
+        // is account disabled ?
+        if ($this->user->disabled=="Yes") {
+            $this->log_failed_access ($username);
+            $this->Log->write( _("login"), _("User account is disabled"), 2, $username );
+            $this->Result->show("danger", _("User account is disabled"), true);
+        }
+        // is passkey login enforced ?
+        elseif ($this->settings->{'passkeys'}=="1") {
+            if ($this->user->passkey_only=="1") {
+                // check passkeys
+                $user_passkeys = $this->get_user_passkeys($this->user->id);
+
+                // make sure it has passkeys configured
+                if (sizeof($user_passkeys)>0) {
+                    $this->log_failed_access ($username);
+                    $this->Log->write( _("Passkey login"), _("Passkey required for login"), 2, $username );
+                    $this->Result->show("danger", _("Only passkey authentication is possible for this account"), true);
+                }
+            }
+        }
+    }
+
+    /**
+     * Process succesfull passkey auth
+     * @method auth_passkey_success
+     * @param  string $encodedCredential
+     * @return bool
+     */
+    public function auth_passkey ($credentialId = "", $encodedCredential = "", $keyId = "") {
+        # save passkey
+        $this->update_passkey ($credentialId, $encodedCredential);
+
+        # get user details from authenticated user_id
+        $this->fetch_passkey_user_details ();
+
+        # failure
+        if(!isset($this->user->username)) {
+            throw new Exception ("Cannot fetch credentials from userid");
+        }
+            header('HTTP/1.1 500 Cannot fetch credentials from userid');
+
+        # set session parameters
+        $_SESSION['ipamusername'] = $this->user->username;
+        $_SESSION['ipamlanguage'] = $this->fetch_lang_details ();
+        $_SESSION['keyId']        = $keyId;
+        $_SESSION['lastactive']   = time();
+
+        # remove passkey temp session user id
+        $this->clear_passkey_user_id ();
+
+        # save to session
+        $this->write_session_parameters ();
+        # log
+        $this->Log->write( _("User login"), _("User")." ".$this->user->real_name." "._("logged in"), 0, $username );
+
+        # write last logintime
+        $this->update_login_time ();
+
+        # remove possible blocked IP
+        $this->block_remove_entry ();
+
+        # ok
+        return true;
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+    /* @passkey -------------------- */
+
+
+    /**
+     * Fetch user details based on passkey ID
+     * @method fetch_passkey_user_details
+     * @return obj
+     */
+    private function fetch_passkey_user_details () {
+        try {
+            $user = $this->Database->getObject("users", $this->get_passkey_user_id());
+
+            if(!is_null($user)) {
+                $this->user = $user;
+            }
+            else {
+                header('HTTP/1.1 404 Not found');
+                $this->block_ip ();
+                $this->Log->write ( _("User login"), _('Failed passkey login'), 2, $this->get_passkey_user_id() );
+            }
+        }
+        catch (Exception $e) {
+            header('HTTP/1.1 500 '.$e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Get passkeys for user
+     * @method get_user_passkeys
+     * @param  bool $user_id
+     * @return array
+     */
+    public function get_user_passkeys ($user_id = false) {
+        // set userId
+        $user_id = $user_id===false ? $this->user->id : $user_id;
+        try {
+            return $this->Database->findObjects("passkeys", "user_id", $user_id);
+        }
+        catch (Exception $e) {
+             !$this->debugging ? : $this->Result->show("danger", $e->getMessage(), false);
+        }
+    }
+
+    /**
+     * Get passkey for user based on key_id
+     * @method get_user_passkeys
+     * @param  bool $user_id
+     * @return array
+     */
+    public function get_user_passkey_by_keyId ($keyId = false) {
+        try {
+            return $this->Database->findObject("passkeys", "keyId", $keyId);
+        }
+        catch (Exception $e) {
+             !$this->debugging ? : $this->Result->show("danger", $e->getMessage(), false);
+        }
+    }
+
+    /**
+     * Save new passkey
+     * @method save_passkey
+     * @param  string $credential
+     * @return bool
+     */
+    public function save_passkey ($credential = "", $credentialId = NULL, $keyId = NULL) {
+        try {
+            $this->Database->insertObject("passkeys", ["user_id"=>$this->user->id, "credentialId"=>$credentialId, "credential"=>$credential, "keyId"=>$keyId, "created"=>date("Y-m-d H:i:s§")]);
+            // ok
+            return true;
+        }
+        catch (Exception $e) {
+            header('HTTP/1.1 500 '.$e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Rename passkey
+     * @method rename_passkey
+     * @param  int $id
+     * @param  string $comment
+     * @return bool
+     */
+    public function rename_passkey ($id = 0, $comment = "") {
+        try {
+            $this->Database->updateObject("passkeys", ["id"=>$id, "comment"=>$comment]);
+            return true;
+        }
+        catch (Exception $e) {
+            $this->debugging ? : $this->Result->show("danger", _("Database error: ").$e->getMessage(), false);
+            return false;
+        }
+    }
+
+    /**
+     * Delete passkey
+     * @method delete_passkey
+     * @param  int $id
+     * @return bool
+     */
+    public function delete_passkey ($id = 0) {
+        try {
+            $this->Database->deleteObject("passkeys", $id);
+            return true;
+        }
+        catch (Exception $e) {
+            $this->debugging ? : $this->Result->show("danger", _("Database error: ").$e->getMessage(), false);
+            return false;
+        }
+    }
+
+    /**
+     * Update passkey on succesfull login
+     * @method save_passkey
+     * @param  string $credential
+     * @return bool
+     */
+    public function update_passkey ($credentialId = "", $updated_credential = "") {
+        try {
+            $this->Database->updateObject("passkeys", ["credentialId"=>$credentialId, "credential"=>$updated_credential, "used"=>date("Y-m-d H:i:s")], "credentialId");
+            // ok
+            return true;
+        }
+        catch (Exception $e) {
+            header('HTTP/1.1 500 '.$e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Save authneitcation user id to session
+     * @method set_passkey_user_id
+     * @param  int $userid
+     */
+    public function set_passkey_user_id ($userid = 0) {
+        $_SESSION['passkey_user_id'] = $userid;
+    }
+
+    /**
+     * Return user id
+     * @method get_passkey_user_id
+     * @return int
+     */
+    public function get_passkey_user_id () {
+        return $_SESSION['passkey_user_id'];
+    }
+
+    /**
+     * Remove temporary clear_passkey_user_id
+     * @method clear_passkey_user_id
+     * @return [type]
+     */
+    public function clear_passkey_user_id () {
+        unset($_SESSION['passkey_user_id']);
+    }
 
 
 
@@ -1355,6 +1609,7 @@ class User extends Common_functions {
                         "menuCompact"      => $this->verify_checkbox(@$post['menuCompact']),
                         "theme"            => $post['theme'],
                         "2fa"              => $this->verify_checkbox(@$post['2fa']),
+                        "passkey_only"     => $this->verify_checkbox(@$post['passkey_only']),
                         );
         if(!is_blank($post['password1'])) {
         $items['password'] = $this->crypt_user_pass ($post['password1']);
@@ -1431,8 +1686,6 @@ class User extends Common_functions {
 
 
 
-
-
     /**
      *    @blocking IP functions
      *    ------------------------------
@@ -1829,6 +2082,7 @@ class User extends Common_functions {
         // return
         return $level=="0" ? "<span class='badge badge1 badge5 alert-danger'>"._($this->parse_permissions ($level))."</span>" : "<span class='badge badge1 badge5 alert-success'>"._($this->parse_permissions ($level))."</span>";
     }
+
 }
 /**
  * Fake User object for install/scripts
diff --git a/functions/composer.json b/functions/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..74c9d4e0a66373dbe8275fed517a1be4377f35af
--- /dev/null
+++ b/functions/composer.json
@@ -0,0 +1,5 @@
+{
+    "require": {
+        "firehed/webauthn": "dev-main"
+    }
+}
diff --git a/functions/composer.lock b/functions/composer.lock
new file mode 100644
index 0000000000000000000000000000000000000000..ebb1323a00e8ad91408ed5f10b4fbfaaaec5ef76
--- /dev/null
+++ b/functions/composer.lock
@@ -0,0 +1,122 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "7bf730768732814406175c12775f3390",
+    "packages": [
+        {
+            "name": "firehed/cbor",
+            "version": "0.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Firehed/cbor-php.git",
+                "reference": "eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Firehed/cbor-php/zipball/eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80",
+                "reference": "eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80",
+                "shasum": ""
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.1"
+            },
+            "suggest": {
+                "ext-bcmath": "Enables parsing of very large values"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Firehed\\CBOR\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eric Stern",
+                    "email": "eric@ericstern.com"
+                }
+            ],
+            "description": "CBOR decoder",
+            "homepage": "https://github.com/Firehed/CBOR",
+            "keywords": [
+                "cbor"
+            ],
+            "support": {
+                "issues": "https://github.com/Firehed/cbor-php/issues",
+                "source": "https://github.com/Firehed/cbor-php/tree/master"
+            },
+            "time": "2019-05-14T06:31:13+00:00"
+        },
+        {
+            "name": "firehed/webauthn",
+            "version": "dev-main",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Firehed/webauthn-php.git",
+                "reference": "267d04a6d2926d9ab6d7630fb86a92410eb6b36c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Firehed/webauthn-php/zipball/267d04a6d2926d9ab6d7630fb86a92410eb6b36c",
+                "reference": "267d04a6d2926d9ab6d7630fb86a92410eb6b36c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-hash": "*",
+                "ext-openssl": "*",
+                "firehed/cbor": "^0.1.0",
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "maglnet/composer-require-checker": "^4.1",
+                "mheap/phpunit-github-actions-printer": "^1.5",
+                "nikic/php-parser": "^4.14",
+                "phpstan/phpstan": "^1.0",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpstan/phpstan-strict-rules": "^1.0",
+                "phpunit/phpunit": "^9.3",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "default-branch": true,
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Firehed\\WebAuthn\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eric Stern",
+                    "email": "eric@ericstern.com"
+                }
+            ],
+            "description": "Web Authentication",
+            "support": {
+                "issues": "https://github.com/Firehed/webauthn-php/issues",
+                "source": "https://github.com/Firehed/webauthn-php/tree/main"
+            },
+            "time": "2023-11-16T23:07:44+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {
+        "firehed/webauthn": 20
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.6.0"
+}
diff --git a/functions/composer.phar b/functions/composer.phar
new file mode 100755
index 0000000000000000000000000000000000000000..e766506542d36f55d0200e9986b5c33a6d2919a1
Binary files /dev/null and b/functions/composer.phar differ
diff --git a/functions/functions.php b/functions/functions.php
index 3a673f9898b27b6735b5baac62a5d3849fbfaf9e..91aa1161395237cdaba5c6e5a0138da516bea148 100755
--- a/functions/functions.php
+++ b/functions/functions.php
@@ -39,7 +39,7 @@ if(php_sapi_name()!="cli")
 if(Config::ValueOf('debugging')==true) {
 	ini_set('display_errors', 1);
 	ini_set('display_startup_errors', 1);
-	error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
+	error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
 }
 else {
 	disable_php_errors();
diff --git a/functions/locale/changes.txt b/functions/locale/changes.txt
index 5fcd1cdc91e097e3ba36927cff165e9abe31727b..34a1411aa54ff4baa74c46113fe49d6d2436a737 100644
--- a/functions/locale/changes.txt
+++ b/functions/locale/changes.txt
@@ -5514,3 +5514,192 @@ msgstr ""
 msgid "Scan agents"
 msgstr ""
 
+
+
+### 1.6.1
+
+msgid "passkey"
+msgstr ""
+
+msgid "Passkey"
+msgstr ""
+
+msgid "2fa account status"
+msgstr ""
+
+msgid "2fa disabled"
+msgstr ""
+
+msgid "2fa enabled"
+msgstr ""
+
+msgid "2fa status"
+msgstr ""
+
+msgid "Account"
+msgstr ""
+
+msgid "Check passkey you want to remove"
+msgstr ""
+
+msgid "Details for your preferred authenticator application are below. Please write down your details, otherwise you will not be able to login to phpipam"
+msgstr ""
+
+msgid "Disable 2fa for user"
+msgstr ""
+
+msgid "Enable Passkeys"
+msgstr ""
+
+msgid "Enable Vaults"
+msgstr ""
+
+msgid "Enable Vaults for storing encrypted information"
+msgstr ""
+
+msgid "Enable passkeys for passwordless login"
+msgstr ""
+
+msgid "failed"
+msgstr ""
+
+msgid "Failed passkey login"
+msgstr ""
+
+msgid "has logged out"
+msgstr ""
+
+msgid "Here you can change settings for two-factor authentication and get your 2fa secret."
+msgstr ""
+
+msgid "Invalid theme"
+msgstr ""
+
+msgid "logged in"
+msgstr ""
+
+msgid "Login with a passkey"
+msgstr ""
+
+msgid "Only passkey authentication is possible for this account"
+msgstr ""
+
+msgid "Passkey login only"
+msgstr ""
+
+msgid "Passkey only"
+msgstr ""
+
+msgid "Passkey required for login"
+msgstr ""
+
+msgid "Passkeys"
+msgstr ""
+
+msgid "Select to only allow account login with passkey"
+msgstr ""
+
+msgid "successful"
+msgstr ""
+
+msgid "There are no passkeys set for user. Resetting passkey login only to false."
+msgstr ""
+
+msgid "Two-factor authentication"
+msgstr ""
+
+msgid "User account is disabled"
+msgstr ""
+
+msgid "User permissions for phpipam modules"
+msgstr ""
+
+msgid "You can also scan following QR code with your preferred authenticator application"
+msgstr ""
+
+msgid "You can login to your account with normal authentication method only untill you create passkeys."
+msgstr ""
+
+msgid "You can only login to your account using passkeys"
+msgstr ""
+
+msgid "Passwordless authentication"
+msgstr ""
+
+msgid "Account details"
+msgstr ""
+
+msgid "You can login to your account with with passkeys only"
+msgstr ""
+
+msgid "This can be changed under Account details tab"
+msgstr ""
+
+msgid "Your passkeys"
+msgstr ""
+
+msgid "Added on"
+msgstr ""
+
+msgid "Last used"
+msgstr ""
+
+msgid "You authenticated with this passkey"
+msgstr ""
+
+msgid "Add a passkey"
+msgstr ""
+
+msgid "Rename"
+msgstr ""
+
+msgid "Here you can manage passkey authentication for your account"
+msgstr ""
+
+msgid "Passkeys are a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN"
+msgstr ""
+
+msgid "Name your passkey"
+msgstr ""
+
+msgid "Duplicates"
+msgstr ""
+
+msgid "Duplicated subnets"
+msgstr ""
+
+msgid "Duplicated hosts"
+msgstr ""
+
+msgid "No duplicate subnets found"
+msgstr ""
+
+msgid "Routing"
+msgstr ""
+
+msgid "BGP routing"
+msgstr ""
+
+msgid "Add peer"
+msgstr ""
+
+msgid "Peer name"
+msgstr ""
+
+msgid "Peer AS"
+msgstr ""
+
+msgid "Local AS"
+msgstr ""
+
+msgid "Peer address"
+msgstr ""
+
+msgid "Local address"
+msgstr ""
+
+msgid "Local Address"
+msgstr ""
+
+msgid "BGP type"
+msgstr ""
diff --git a/functions/locale/en_GB.UTF-8/LC_MESSAGES/phpipam.po b/functions/locale/en_GB.UTF-8/LC_MESSAGES/phpipam.po
index e6dba6dc83f47cf19d571bf516263b549c9a77e0..b099a804cf515241c23db1b442bf1fd6f9187bef 100644
--- a/functions/locale/en_GB.UTF-8/LC_MESSAGES/phpipam.po
+++ b/functions/locale/en_GB.UTF-8/LC_MESSAGES/phpipam.po
@@ -1,4 +1,4 @@
-# PHPIPAM translations file - template
+# PHPIPAM translations file - English
 # Copyright (C) phpipam 2013
 # This file is distributed under the same license as the phpipam package.
 # miha petkovsek <miha.petkovsek@gmail.com>, 2013.
@@ -7,72 +7,13 @@
 # "Project-Id-Version: 1.40\n"
 # "Report-Msgid-Bugs-To: Miha Petkovsek <miha.petkovsek@gmail.com>\n"
 # "POT-Creation-Date: 2013-04-02 14:33+0200\n"
-# "PO-Revision-Date: 2015-10-07 14:33+0200\n"
-# "Last-Translator: \n"
-# "Language: English\n"
+# "PO-Revision-Date: 2017-02-21 14:33+0200\n"
+# "Last-Translator: Miha Petkovsek <miha.petkovsek@gmail.com>\n"
+# "Language: Slovenian (sl_SI)\n"
 # "MIME-Version: 1.0\n"
 # "Content-Type: text/plain; charset=UTF-8\n"
 # "Content-Transfer-Encoding: 8bit\n"
 
-#: functions/functions-install.php:85 functions/functions-install.php:133
-#: functions/functions-install.php:150 functions/functions-install.php:182
-#: functions/functions-install.php:199 functions/functions-install.php:236
-#: functions/functions-install.php:272 functions/functions-install.php:379
-#: functions/functions-install.php:453 functions/functions-install.php:495
-#: functions/functions-install.php:524 functions/functions-install.php:549
-#: functions/functions-install.php:576 functions/functions-admin.php:41
-#: functions/functions-admin.php:73 functions/functions-admin.php:147
-#: functions/functions-admin.php:185 functions/functions-admin.php:217
-#: functions/functions-admin.php:241 functions/functions-admin.php:387
-#: functions/functions-admin.php:507 functions/functions-admin.php:533
-#: functions/functions-admin.php:887 functions/functions-admin.php:1124
-#: functions/functions-admin.php:1204 functions/functions-admin.php:1238
-#: functions/functions-admin.php:1283 functions/functions-admin.php:1374
-#: functions/functions-admin.php:1493 functions/functions-admin.php:1633
-#: functions/functions-admin.php:1661 functions/functions-admin.php:1717
-#: functions/functions-admin.php:1750 functions/functions-admin.php:1848
-#: functions/functions-admin.php:1881 functions/functions-admin.php:1980
-#: functions/functions-admin.php:2012 functions/functions-admin.php:2106
-#: functions/functions-admin.php:2138 functions/functions-common.php:105
-#: functions/functions-common.php:154 functions/functions-common.php:177
-#: functions/functions-common.php:200 functions/functions-common.php:223
-#: functions/functions-common.php:246 functions/functions-common.php:396
-#: functions/functions-common.php:420 functions/functions-common.php:453
-#: functions/functions-common.php:709 functions/functions-network.php:90
-#: functions/functions-network.php:116 functions/functions-network.php:142
-#: functions/functions-network.php:197 functions/functions-network.php:231
-#: functions/functions-network.php:255 functions/functions-network.php:289
-#: functions/functions-network.php:313 functions/functions-network.php:337
-#: functions/functions-network.php:361 functions/functions-network.php:396
-#: functions/functions-network.php:420 functions/functions-network.php:454
-#: functions/functions-network.php:478 functions/functions-network.php:502
-#: functions/functions-network.php:526 functions/functions-network.php:550
-#: functions/functions-network.php:586 functions/functions-network.php:610
-#: functions/functions-network.php:641 functions/functions-network.php:670
-#: functions/functions-network.php:694 functions/functions-network.php:802
-#: functions/functions-network.php:873 functions/functions-network.php:956
-#: functions/functions-network.php:1252 functions/functions-network.php:1289
-#: functions/functions-network.php:1313 functions/functions-network.php:1366
-#: functions/functions-network.php:1417 functions/functions-network.php:1460
-#: functions/functions-network.php:1487 functions/functions-network.php:1512
-#: functions/functions-network.php:1536 functions/functions-network.php:1562
-#: functions/functions-network.php:1590 functions/functions-network.php:1620
-#: functions/functions-network.php:1729 functions/functions-network.php:1754
-#: functions/functions-network.php:2072 functions/functions-network.php:2181
-#: functions/functions-network.php:2251 functions/functions-tools.php:220
-#: functions/functions-tools.php:241 functions/functions-tools.php:284
-#: functions/functions-tools.php:311 functions/functions-tools.php:333
-#: functions/functions-tools.php:355 functions/functions-tools.php:398
-#: functions/functions-tools.php:429 functions/functions-tools.php:453
-#: functions/functions-tools.php:478 functions/functions-tools.php:620
-#: functions/functions-tools.php:644 functions/functions-tools.php:666
-#: functions/functions-tools.php:688 functions/functions-tools.php:711
-#: functions/functions-tools.php:740 functions/functions-tools.php:766
-#: functions/functions-tools.php:817 functions/functions-tools.php:856
-#: functions/functions-tools.php:880 functions/functions-tools.php:905
-#: functions/functions-tools.php:941 site/ipaddr/modifyIpAddressCheck.php:102
-#: site/ipaddr/modifyIpAddressCheck.php:103
-#: site/ipaddr/modifyIpAddressCheck.php:179
 msgid "Error"
 msgstr ""
 
@@ -416,7 +357,7 @@ msgstr ""
 #: site/ipaddr/ipAddressPrintTableSlaves.php:34 site/admin/manageSubnet.php:76
 #: site/admin/manageSubnetEdit.php:76
 #: site/admin/manageSubnetEditResult.php:169 site/admin/manageRequests.php:24
-#: site/admin/ripeImportTelnet.php:74 site/admin/manageSubnetsplit.php:64
+#: site/admin/RIPE / ARINImportTelnet.php:74 site/admin/manageSubnetsplit.php:64
 #: site/admin/manageSubnetresize.php:35 site/admin/manageSubnettruncate.php:37
 msgid "Subnet"
 msgstr ""
@@ -437,7 +378,7 @@ msgstr ""
 #: site/admin/manageVRFEdit.php:51 site/admin/manageVRFEdit.php:57
 #: site/admin/manageVLANEdit.php:58 site/admin/manageVLANEdit.php:60
 #: site/admin/manageRequests.php:26 site/admin/replaceFields.php:27
-#: site/admin/ripeImportTelnet.php:90 site/admin/manageSection.php:28
+#: site/admin/RIPE / ARINImportTelnet.php:90 site/admin/manageSection.php:28
 #: site/admin/manageSectionEdit.php:51 site/admin/manageVLANs.php:36
 #: site/admin/manageDevices.php:37 site/admin/groupEditPrint.php:57
 #: site/login/requestIPform.php:82
@@ -597,7 +538,7 @@ msgstr ""
 #: site/ipaddr/mailNotifyIP.php:58 site/ipaddr/subnetDetailsSlaves.php:85
 #: site/ipaddr/subnetDetails.php:96 site/admin/manageSubnet.php:78
 #: site/admin/manageSubnetEdit.php:125 site/admin/manageVLANEdit.php:32
-#: site/admin/ripeImportTelnet.php:95 site/admin/manageVLANEditResult.php:47
+#: site/admin/RIPE / ARINImportTelnet.php:95 site/admin/manageVLANEditResult.php:47
 #: site/admin/manageVLANEditResult.php:48
 msgid "VLAN"
 msgstr ""
@@ -695,6 +636,8 @@ msgstr ""
 #: site/ipaddr/subnetDetails.php:126
 msgid "enabled"
 msgstr ""
+msgid "Enabled"
+msgstr ""
 
 #: site/tools/vrf.php:114
 msgid "No subnets belonging to this VRF"
@@ -878,9 +821,10 @@ msgid "Hostname search tips"
 msgstr ""
 
 #: site/tools/searchTips.php:46
-msgid "You can get all IP addresses some host uses and all ports it is connected to "
+msgid  "You can get all IP addresses some host uses and all ports it is connected to "
 "by entering hostname in search field"
 msgstr ""
+"iščeš po DNS imenu"
 
 #: site/tools/searchTips.php:53
 msgid "Device search tips"
@@ -890,6 +834,7 @@ msgstr ""
 msgid "You can get all used / available ports and connected IP's / hostnames in "
 "some device by entering device name in search field"
 msgstr ""
+"v iskalnik vpiši ime naprave"
 
 #: site/tools/searchTips.php:62
 msgid "MAC search tips"
@@ -899,6 +844,7 @@ msgstr ""
 msgid "You can search by MAC address list entering MAC in 00:1cd:d4:78:ec:46 or "
 "001dd478ec46 format, or search multiple with 00:1c:c4:"
 msgstr ""
+"001dd478ec46 formatu, ali pa poišči vec mac adres z iskanjem npr. 00:1c:c4:"
 
 #: site/tools/searchTips.php:71
 msgid "Custom field search tips"
@@ -937,7 +883,7 @@ msgstr ""
 msgid "Instructions"
 msgstr ""
 
-#: site/tools/toolsMenu.php:31 site/admin/ripeImport.php:19
+#: site/tools/toolsMenu.php:31 site/admin/RIPE / ARINImport.php:19
 #: site/userMenu.php:22 site/sections.php:104
 msgid "Search"
 msgstr ""
@@ -1004,6 +950,7 @@ msgstr ""
 msgid "Search results (Subnet list)"
 msgstr ""
 
+
 #: site/tools/searchResults.php:312
 msgid "Edit subnet details"
 msgstr ""
@@ -1138,8 +1085,9 @@ msgid "Orphaned IP addresses for subnet"
 msgstr ""
 
 #: site/ipaddr/ipAddressPrintTableOrphaned.php:69
-msgid "This happens if subnet had IP addresses<br>when new nested subnet was added"
+msgid  "This happens if subnet had IP addresses<br>when new nested subnet was added"
 msgstr ""
+"Do tega pride, če se z izbrisom omrežja<br>niso pobrisali tudi IP naslovi"
 
 #: site/ipaddr/ipAddressPrintTableOrphaned.php:171
 msgid "Move to different subnet"
@@ -1285,11 +1233,11 @@ msgid "Move IP address"
 msgstr ""
 
 #: site/ipaddr/modifyIpAddress.php:31
-msgid "Cannot edit IP address details"
+msgid "You do not have write access for this network'"
 msgstr ""
 
 #: site/ipaddr/modifyIpAddress.php:31
-msgid "You do not have write access for this network'"
+msgid "Cannot edit IP address details"
 msgstr ""
 
 #: site/ipaddr/modifyIpAddress.php:79 site/ipaddr/modifyIpAddress.php:80
@@ -1308,6 +1256,7 @@ msgstr ""
 msgid "You can add,edit or delete multiple IP addresses<br>by specifying IP range "
 "(e.g. 10.10.0.0-10.10.0.25)"
 msgstr ""
+"(npr.: 10.10.0.0-10.10.0.25)"
 
 #: site/ipaddr/modifyIpAddress.php:147
 msgid "Click to check for hostname"
@@ -1441,8 +1390,9 @@ msgstr ""
 msgid "If you like the software you can donate by clicking this button to support "
 "further development"
 msgstr ""
+"tem podprete nadaljnji razvoj projekta"
 
-#: site/dashboard/widgets/statistics.php:27
+#: site/dashboard/statistics.php:27
 msgid "Number of Sections"
 msgstr ""
 
@@ -1506,7 +1456,7 @@ msgstr ""
 msgid "No IPv4 host configured"
 msgstr ""
 
-#: site/dashboard/top10_hosts.php:153
+#: site/dashboard/top10_percentage.php:189
 msgid "No IPv6 host configured"
 msgstr ""
 
@@ -1570,16 +1520,17 @@ msgstr ""
 msgid "Missing fields"
 msgstr ""
 
-#: site/admin/ripeImport.php:12
-msgid "Import subnets from RIPE"
+#: site/admin/RIPE / ARINImport.php:12
+msgid "Import subnets from RIPE / ARIN"
 msgstr ""
 
-#: site/admin/ripeImport.php:15
-msgid "This script imports subnets from RIPE database for specific AS. Enter "
+#: site/admin/RIPE / ARINImport.php:15
+msgid "This script imports subnets from RIPE / ARIN database for specific AS. Enter "
 "desired AS to search for subnets"
 msgstr ""
+"Vnesite žljeni AS za iskanje pripadajočih omrežij"
 
-#: site/admin/ripeImport.php:19
+#: site/admin/RIPE / ARINImport.php:19
 msgid "AS number"
 msgstr ""
 
@@ -1649,17 +1600,21 @@ msgstr ""
 msgid "Here you can set parameters for connecting to AD for authenticating users. "
 "phpIPAM uses"
 msgstr ""
+"phpIPAM uporablja"
 
 #: site/admin/manageAD_AD.php:17 site/admin/manageAD_LDAP.php:17
 msgid "to authenticate users. If you need additional settings please take a look at "
 "functions/adLDAP or check online documentation!"
 msgstr ""
+"v functions/adLDAP, ali pa preverite dokumentacijo"
 
 #: site/admin/manageAD_AD.php:20
 msgid "First create new user under user management with <u>same username as on AD</u>"
 " and set usertype to domain user. Also set proper permissions - group membership "
 "for new user."
 msgstr ""
+" in za tip uporabnika izberite domenski uporabnik. Nastavite tudi pravilne pravice dostopa - "
+"pripadnost skupinam"
 
 #: site/admin/manageAD_AD.php:29 site/admin/manageAD_LDAP.php:29
 msgid "ldap extension not enabled in php"
@@ -1673,6 +1628,7 @@ msgstr ""
 msgid "Enter domain controllers, separated by ; (default: dc1.domain.local;dc2."
 "domain.local)"
 msgstr ""
+"domain.local)"
 
 #: site/admin/manageAD_AD.php:49 site/admin/manageAD_LDAP.php:48
 msgid "Base DN"
@@ -1684,6 +1640,9 @@ msgid "Enter base DN for LDAP (default: CN=Users,CN=Company,DC=domain,DC=local)"
 "\t\tIf this is set to null then adLDAP will attempt to obtain this "
 "automatically from the rootDSE"
 msgstr ""
+"<br>\n"
+"\t\tČe je to polje prazno bo adLDAP poizkušal avtomatsko pridobiti to informacijo "
+"iz korenskega DSE"
 
 #: site/admin/manageAD_AD.php:61
 msgid "Account suffix"
@@ -1719,6 +1678,7 @@ msgstr ""
 msgid "If you wish to use TLS you should ensure that useSSL is set to false and "
 "vice-versa (default: false)"
 msgstr ""
+"(privzeto: ne)"
 
 #: site/admin/manageAD_AD.php:102
 msgid "AD port"
@@ -1803,6 +1763,7 @@ msgid "You can select which fields are actually being used for IP management, so
 "you dont show any overhead if not used. IP, hostname and description are "
 "mandatory"
 msgstr ""
+"manj pomembna ostanejo skrita. IP naslov, dns ime in opis so obvezna polja."
 
 #: site/admin/filterIPFields.php:44
 msgid "Check which fields to use for IP addresses"
@@ -1913,6 +1874,7 @@ msgstr ""
 msgid "Normal users will have permissions set based on group access to sections and "
 "subnets"
 msgstr ""
+"in pravice skupin"
 
 #: site/admin/manageSubnet.php:11 site/admin/adminMenu.php:53
 msgid "Subnet management"
@@ -1983,7 +1945,7 @@ msgid "VRF management"
 msgstr ""
 
 #: site/admin/adminMenu.php:68
-msgid "RIPE import"
+msgid "RIPE / ARIN import"
 msgstr ""
 
 #: site/admin/adminMenu.php:77
@@ -2046,6 +2008,7 @@ msgstr ""
 msgid "Domain authenticates on AD, but still needs to be setup here for permissions "
 "etc."
 msgstr ""
+"pravic dostopa ipd."
 
 #: site/admin/usersEditPrint.php:116
 msgid "User's password"
@@ -2082,6 +2045,7 @@ msgstr ""
 #: site/admin/usersEditPrint.php:150
 msgid "Users have access defined based on groups"
 msgstr ""
+"skupin, katerim pripadajo"
 
 #: site/admin/usersEditPrint.php:177
 msgid "No groups configured"
@@ -2140,7 +2104,7 @@ msgid "subnet in CIDR"
 msgstr ""
 
 #: site/admin/manageSubnetEdit.php:86
-msgid "Get information from RIPE database"
+msgid "Get information from RIPE / ARIN database"
 msgstr ""
 
 #: site/admin/manageSubnetEdit.php:87
@@ -2183,6 +2147,7 @@ msgstr ""
 msgid "Enter master subnet if you want to nest it under existing subnet, or select "
 "root to create root subnet"
 msgstr ""
+"korensko omrežje za izdelavo novega"
 
 #: site/admin/manageSubnetEdit.php:188
 msgid "Select VRF"
@@ -2239,6 +2204,7 @@ msgstr ""
 msgid "Removing subnets will delete ALL underlaying subnets and belonging IP "
 "addresses"
 msgstr ""
+"naslove"
 
 #: site/admin/manageSubnetEdit.php:324
 msgid "Delete subnet"
@@ -2264,7 +2230,7 @@ msgstr ""
 msgid "Status"
 msgstr ""
 
-#: site/admin/CSVimportShowFile.php:100 site/admin/ripeImportTelnet.php:108
+#: site/admin/CSVimportShowFile.php:100 site/admin/RIPE / ARINImportTelnet.php:108
 msgid "Import to database"
 msgstr ""
 
@@ -2416,7 +2382,7 @@ msgstr ""
 msgid "Errors occured when importing to database!"
 msgstr ""
 
-#: site/admin/CSVimportSubmit.php:95 site/admin/ripeImportResult.php:72
+#: site/admin/CSVimportSubmit.php:95 site/admin/RIPE / ARINImportResult.php:72
 msgid "Import successfull"
 msgstr ""
 
@@ -2631,6 +2597,7 @@ msgstr ""
 msgid "Set authentication type for users. Requires php LDAP support. Set connection "
 "settings in admin menu"
 msgstr ""
+"za php. Nastavite nastavitve povezave v administracijskem meniju"
 
 #: site/admin/settings.php:105
 msgid "Tooltips"
@@ -2665,6 +2632,7 @@ msgid "Check reverse dns lookups for IP addresses that do not have hostname in "
 "database. (Activating this feature can significantly increase ip address "
 "pages loading time!)"
 msgstr ""
+"podatkovni bazi. (Aktivacija te funkcije lahko povzroči precej daljše nalaganje strani!)"
 
 #: site/admin/settings.php:149
 msgid "Duplicate VLANs"
@@ -2698,6 +2666,7 @@ msgstr ""
 msgid "Select netmask limit for visual display of IP addresses (mask equal or "
 "bigger than - more then /22 not recommended)"
 msgstr ""
+" - večja od /22 ni priporočena)"
 
 #: site/admin/settings.php:212
 msgid "IP address print limit"
@@ -2771,19 +2740,19 @@ msgstr ""
 msgid "Instructions updated successfully"
 msgstr ""
 
-#: site/admin/ripeImportTelnet.php:48
+#: site/admin/RIPE / ARINImportTelnet.php:48
 msgid "No subnets found"
 msgstr ""
 
-#: site/admin/ripeImportTelnet.php:56
+#: site/admin/RIPE / ARINImportTelnet.php:56
 msgid "I found the following routes belonging to AS"
 msgstr ""
 
-#: site/admin/ripeImportTelnet.php:69
+#: site/admin/RIPE / ARINImportTelnet.php:69
 msgid "Remove this subnet"
 msgstr ""
 
-#: site/admin/ripeImportTelnet.php:79
+#: site/admin/RIPE / ARINImportTelnet.php:79
 msgid "select section"
 msgstr ""
 
@@ -2830,6 +2799,7 @@ msgstr ""
 #: site/admin/manageSection.php:92
 msgid "If group is not set in permissions then it will not have access to subnet"
 msgstr ""
+"dostopa do omrežja"
 
 #: site/admin/manageSection.php:93
 msgid "Groups with RO permissions will not be able to create new subnets"
@@ -2839,6 +2809,7 @@ msgstr ""
 msgid "Subnet permissions must be set separately. By default if group has access to "
 "section<br>it will have same permission on subnets"
 msgstr ""
+"do razdelka<br>, potem bo imela enake pravice v pripadajočih omrežjih"
 
 #: site/admin/manageSection.php:95
 msgid "You can choose to delegate section permissions to all underlying subnets"
@@ -2848,6 +2819,7 @@ msgstr ""
 msgid "If group does not have access to section it will not be able to access "
 "subnet, even if<br>subnet permissions are set"
 msgstr ""
+"tudi če so pravice za to omrežje določene"
 
 #: site/admin/customSubnetFieldsOrder.php:15
 #: site/admin/customIPFieldsOrder.php:15
@@ -2939,6 +2911,7 @@ msgstr ""
 msgid "No disables overlapping subnet checks. Subnets can be nested/created "
 "randomly. Anarchy."
 msgstr ""
+"poljubno. Anarhija."
 
 #: site/admin/manageSectionEdit.php:90
 msgid "Permissions"
@@ -2970,11 +2943,11 @@ msgstr ""
 msgid "Logs cleared successfully"
 msgstr ""
 
-#: site/admin/ripeImportResult.php:66
+#: site/admin/RIPE / ARINImportResult.php:66
 msgid "Failed to import subnet"
 msgstr ""
 
-#: site/admin/ripeImportResult.php:76
+#: site/admin/RIPE / ARINImportResult.php:76
 msgid "Please fix the following errors before inserting"
 msgstr ""
 
@@ -2998,11 +2971,13 @@ msgstr ""
 msgid "Here you can set parameters for connecting to OpenLDAP for authenticating "
 "users. phpIPAM uses"
 msgstr ""
+"uporabnikov. phpIPAM uporablja "
 
 #: site/admin/manageAD_LDAP.php:20
 msgid "First create new user under user management with <u>same username as on "
 "LDAP</u> and set usertype to domain user. Also set proper groups (premissions) for this user."
 msgstr ""
+"LDAPu</u> in izberite tip uporabnika domenski uporabnik. Nastavite tudi ustrezne skupine za uporabnika"
 
 #: site/admin/manageAD_LDAP.php:38
 msgid "LDAP servers"
@@ -3108,6 +3083,7 @@ msgstr ""
 msgid "To successfully import data please use the following XLS/CSV structure:<br>"
 "( ip | State | Description | hostname | MAC | Owner | Device | Port | Note "
 msgstr ""
+"( ip | State | Opis | DNS ime | MAC | Lastnik | Naprava | Port | Note "
 
 #: site/admin/CSVimport.php:48
 msgid "Upload file"
@@ -3185,6 +3161,7 @@ msgstr ""
 msgid "If existing IP will fall to subnet/broadcast of new subnets split will fail, "
 "except if strict mode is disabled"
 msgstr ""
+"ne bo uspela, razen če je strog način izklopljen"
 
 #: site/admin/manageSubnetresize.php:41
 msgid "Current mask"
@@ -3207,6 +3184,7 @@ msgstr ""
 msgid "If strict mode is enabled check will be made to ensure it is still inside "
 "master subnet"
 msgstr ""
+"še vedno znotraj nadrejenega omrežja"
 
 #: site/admin/manageDevices.php:17
 msgid "Add device"
@@ -3266,6 +3244,7 @@ msgstr ""
 #: site/admin/usersEditEmailNotif.php:28
 msgid "Sending notification mail for new account failed"
 msgstr ""
+"računu ni uspelo"
 
 #: site/admin/usersEditEmailNotif.php:29
 msgid "Notification mail for new account sent"
@@ -3592,7 +3571,7 @@ msgstr ""
 msgid "Languages"
 msgstr ""
 
-#: site/admin/languages.php
+#: site/admin/languages
 msgid "Manage translations"
 msgstr ""
 
@@ -3704,31 +3683,31 @@ msgstr ""
 msgid "IP delete successful"
 msgstr ""
 
-#: site/admin/subnetDetailsSlaves.php
+#: site/admin/subnetDetailsSlaves.php:134
 msgid "You do not have permissions to truncate subnet"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Failed to add device"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Failed to edit device"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Failed to delete device"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Device add successfull"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Device edit successfull"
 msgstr ""
 
-#: site/admin/manageDevicesEditResult.php
+#: site/admin/manageDevicesEditResult
 msgid "Device delete successfull"
 msgstr ""
 
@@ -3858,7 +3837,7 @@ msgstr ""
 msgid "Ping hosts inside subnet to check availability"
 msgstr ""
 
-#: site/ipaddr/subnetDetials.php
+#: site/ipaddr/subnetDetails.php
 msgid "Hosts check"
 msgstr ""
 
@@ -4010,6 +3989,11 @@ msgstr ""
 msgid "Invalid ping path"
 msgstr ""
 
+
+
+
+
+
 #: site/admin/settings.php
 msgid "Display settings"
 msgstr ""
@@ -4248,7 +4232,7 @@ msgstr ""
 msgid "Required field"
 msgstr ""
 
-#: site/admin/custopmIPFields.php
+#: site/admin/customIPFields.php
 msgid "Required"
 msgstr ""
 
@@ -4341,7 +4325,10 @@ msgstr ""
 msgid "Delete folder"
 msgstr ""
 
-#: site/admin/userADsearchForm.php
+#: site/admin/CSVimportSubmit.php
+msgid "Errors marked with red will be ignored from importing"
+msgstr ""
+
 msgid "Search user in AD"
 msgstr ""
 
@@ -4391,6 +4378,9 @@ msgstr ""
 msgid "No changelog entries are available for this section"
 msgstr ""
 
+msgid "No changelog entries are available"
+msgstr ""
+
 #: site/ipaddr/ipDetails.php
 msgid "Availability"
 msgstr ""
@@ -4587,11 +4577,11 @@ msgstr ""
 msgid "Sender mail"
 msgstr ""
 
-#: site/admin/settings.php
+#: site/admin.settings.php
 msgid "Set administrator name"
 msgstr ""
 
-msgid "Set administrator email"
+msgid "Set administrator e-mail"
 msgstr ""
 
 #: site/admin/verifyDatabase.php
@@ -4628,17 +4618,14 @@ msgstr ""
 msgid "Sort by vendor"
 msgstr ""
 
-#: site/admin/userEditPrint.php
-msgid "Mail State changes"
-msgstr ""
-
-msgid "Select yes to receive notification change mail for State change"
+msgid "Select yes to receive notification change mail for"
 msgstr ""
 
-msgid "Mail Changelog"
+msgid "IP edited, Subnet edited, State change"
 msgstr ""
 
-msgid "Select yes to receive notification change mail for changelog"
+#: site/admin/userEditPrint.php
+msgid "Mail State changes"
 msgstr ""
 
 #: site/admin/usersEditPrint.php
@@ -4720,17 +4707,6 @@ msgstr ""
 msgid "Discover new hosts in this subnet"
 msgstr ""
 
-
-## 1.18.00
-
-
-
-
-
-
-### 1.18.006 ###
-
-
 msgid "Invalid ID"
 msgstr ""
 
@@ -4941,9 +4917,6 @@ msgstr ""
 msgid "No subnets"
 msgstr ""
 
-msgid "No changelog entries are available"
-msgstr ""
-
 msgid "Device details"
 msgstr ""
 
@@ -4974,9 +4947,6 @@ msgstr ""
 msgid "Invalid widget"
 msgstr ""
 
-msgid "No $type hosts configured"
-msgstr ""
-
 msgid "Loading statistics"
 msgstr ""
 
@@ -5175,9 +5145,6 @@ msgstr ""
 msgid "Master NS"
 msgstr ""
 
-msgid "NULL"
-msgstr ""
-
 msgid "Domain type"
 msgstr ""
 
@@ -5610,7 +5577,7 @@ msgstr ""
 msgid "For AD/LDAP connection phpipam is using adLDAP, for documentation please check "
 msgstr ""
 
-msgid "First create new user under user management with <u>same username as on AD</u> and set authention type to one of available methods."
+msgid "First create new user under user management with <u>same username as on AD</u> and set authention typeto one of available methods."
 msgstr ""
 
 msgid "Failed to edit authentication method"
@@ -5910,9 +5877,6 @@ msgstr ""
 msgid "Invalid file type"
 msgstr ""
 
-msgid "Errors marked with red will be ignored from importing"
-msgstr ""
-
 msgid "Invalid subnet ID"
 msgstr ""
 
@@ -6009,9 +5973,6 @@ msgstr ""
 msgid "Invalid Network!"
 msgstr ""
 
-msgid "Subnet $new_subnet overlaps with"
-msgstr ""
-
 msgid "Mask must be an integer"
 msgstr ""
 
@@ -6057,7 +6018,8 @@ msgstr ""
 msgid "No subnets belong to this device"
 msgstr ""
 
-# 1.19
+### 1.19.000 ###
+
 msgid "Log files are sent to syslog"
 msgstr ""
 
@@ -6115,7 +6077,7 @@ msgstr ""
 msgid "Enter scan agent name"
 msgstr ""
 
-msgid "Agent description"
+msgid "Agent description'"
 msgstr ""
 
 msgid "Agent type"
@@ -6154,230 +6116,6 @@ msgstr ""
 msgid "Scan agent references removed"
 msgstr ""
 
-### 1.19.002 ###
-
-msgid "Enable Firewall Zones"
-msgstr ""
-
-msgid "Enable or disable firewall zone management module"
-msgstr ""
-
-msgid "Firewall zone management"
-msgstr ""
-
-msgid "Invalid zone alias value."
-msgstr ""
-
-msgid "Invalid interface."
-msgstr ""
-
-msgid "Invalid zone ID."
-msgstr ""
-
-msgid "Invalid mapping ID."
-msgstr ""
-
-msgid "Cannot add mapping"
-msgstr ""
-
-msgid "Mapping modified successfully"
-msgstr ""
-
-msgid "Invalid ID. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Invalid action. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Add a mapping between a firewall device and a firewall zone"
-msgstr ""
-
-msgid "Zone to map"
-msgstr ""
-
-msgid "Select a firewall zone"
-msgstr ""
-
-msgid "Firewall to map"
-msgstr ""
-
-msgid "Select firewall"
-msgstr ""
-
-msgid "Interface"
-msgstr ""
-
-msgid "Create Firewall zone mapping"
-msgstr ""
-
-msgid "Firewall interface"
-msgstr ""
-
-msgid "Zone alias"
-msgstr ""
-
-msgid "Local zone alias"
-msgstr ""
-
-msgid "You are about to remove the firewall to zone mapping!"
-msgstr ""
-
-msgid "Zone"
-msgstr ""
-
-msgid "Alias"
-msgstr ""
-
-msgid "Devicename"
-msgstr ""
-
-msgid "No firewall zones configured"
-msgstr ""
-
-msgid "Invalid zone name length parameter. A valid valid value is between 3 and 31"
-msgstr ""
-
-msgid "Invalid IPv4 address type alias. Only alphanumeric characters, &quot;-&quot;, &quot;_&quot; and &quot;.&quot; are allowed."
-msgstr ""
-
-msgid "Invalid separator. Only &quot;-&quot;, &quot;_&quot; and &quot;.&quot; are allowed."
-msgstr ""
-
-msgid "Invalid zone indicator. Only alphanumeric characters, &quot;-&quot;, &quot;_&quot; and &quot;.&quot; are allowed."
-msgstr ""
-
-msgid "Invalid zone generator method. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Invalid zone generator types [decimal]. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Invalid zone generator types [hex]. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Invalid zone generator types [text]. Do not manipulate the POST values!"
-msgstr ""
-
-msgid "Invalid padding value. Use the checkbox to set the padding value to on or off."
-msgstr ""
-
-msgid "Invalid device type."
-msgstr ""
-
-msgid "Maximum zone name length"
-msgstr ""
-
-msgid "Choose a maximum length of the zone name.<br>The default length is 3, the maximum is 31 characters.<br>(keep in mind that your firewall may have a limit for the length of zone names or address objects )"
-msgstr ""
-
-msgid "IPv4 address type alias"
-msgstr ""
-
-msgid "IPv6 address type alias"
-msgstr ""
-
-msgid "Address type aliases are used to indicate a IPv4 or IPv6 address object."
-msgstr ""
-
-msgid "The separator is used to keep the name of address objects tidy."
-msgstr ""
-
-msgid "Own zone indicator"
-msgstr ""
-
-msgid "The indicator is used to indicate a zone wether is owned by the company or by a customer.<br>It is the leading character of the zone name but will be separated from the zone name in the database."
-msgstr ""
-
-msgid "Customer zone indicator"
-msgstr ""
-
-msgid "Zone generator method"
-msgstr ""
-
-msgid "Generate zone names automaticaly with the setting &quot;decimal&quot; or &quot;hex&quot;.<br>To use your own unique zone names you can choose the option &quot;text&quot."
-msgstr ""
-
-msgid "Zone name padding"
-msgstr ""
-
-msgid "Insert leading zeros into the zone name if you want to have a constant length of your zone name.<br>This setting will be ignored if you use the \"text\" zone name generator."
-msgstr ""
-
-msgid "Zone name strict mode"
-msgstr ""
-
-msgid "Zone name strict mode is enabled by default.<br>If you like to use your own zone names with the &quot;text&quot; mode you may uncheck this to have not unique zone names."
-msgstr ""
-
-msgid "Firewall device Type"
-msgstr ""
-
-msgid "Select the appropriate device type to match firewall devices."
-msgstr ""
-
-msgid "Invalid zone name value."
-msgstr ""
-
-msgid "Invalid indicator ID."
-msgstr ""
-
-msgid "Invalid section ID."
-msgstr ""
-
-msgid "Invalid subnet ID."
-msgstr ""
-
-msgid "Invalid L2 domain ID."
-msgstr ""
-
-msgid "Invalid VLAN ID."
-msgstr ""
-
-msgid "Invalid generator ID."
-msgstr ""
-
-msgid "Invalid padding setting."
-msgstr ""
-
-msgid "Cannot generate zone name"
-msgstr ""
-
-msgid "Cannot validate zone name"
-msgstr ""
-
-msgid "Cannot add zone"
-msgstr ""
-
-msgid "Zone modified successfully"
-msgstr ""
-
-msgid "Add a firewall zone"
-msgstr ""
-
-msgid "Zone name (Only alphanumeric and special characters like .-_ and space.)"
-msgstr ""
-
-msgid "The zone name will be automatically generated"
-msgstr ""
-
-msgid "Zone name"
-msgstr ""
-
-msgid "Own zone"
-msgstr ""
-
-msgid "Customer zone"
-msgstr ""
-
-msgid "Removing this firewall zone will also remove all referenced mappings!"
-msgstr ""
-
-msgid "Create Firewall zone"
-msgstr ""
-
-msgid "Firewall zone and device mappings"
-msgstr ""
-
 msgid "Base DN for your directory"
 msgstr ""
 
@@ -6732,6 +6470,12 @@ msgstr ""
 msgid "NAT settings"
 msgstr ""
 
+msgid "RIPE import"
+msgstr ""
+
+msgid "Import subnets from RIPE"
+msgstr ""
+
 msgid "Select which default address fields to display"
 msgstr ""
 
@@ -7170,6 +6914,9 @@ msgstr ""
 msgid "Automatic"
 msgstr ""
 
+msgid "This script imports subnets from RIPE database for specific AS. Enter desired AS to search for subnets"
+msgstr ""
+
 msgid "Invalid AS"
 msgstr ""
 
@@ -8303,3 +8050,502 @@ msgstr ""
 
 msgid "phpIPAM settings"
 msgstr ""
+
+
+### Customers module ###
+
+msgid "All customers"
+msgstr ""
+
+msgid "Add customer"
+msgstr ""
+
+msgid "Customer"
+msgstr ""
+
+msgid "Customers"
+msgstr ""
+
+msgid "Select customer"
+msgstr ""
+
+msgid "Edit customer"
+msgstr ""
+
+msgid "Delete customer"
+msgstr ""
+
+msgid "Contact"
+msgstr ""
+
+msgid "Customer title"
+msgstr ""
+
+msgid "Customer address"
+msgstr ""
+
+msgid "Contact details"
+msgstr ""
+
+msgid "Customer contact details"
+msgstr ""
+
+msgid "Postcode"
+msgstr ""
+
+msgid "City"
+msgstr ""
+
+msgid "Contact person"
+msgstr ""
+
+msgid "Phone"
+msgstr ""
+
+msgid "Random notes"
+msgstr ""
+
+msgid "Customers module"
+msgstr ""
+
+msgid "Enable or disable customers module for customer management"
+msgstr ""
+
+
+
+### 2FA ###
+
+msgid "2FA authentication"
+msgstr ""
+
+msgid "2FA provider"
+msgstr ""
+
+msgid "Current 2FA provider"
+msgstr ""
+
+msgid "2FA users"
+msgstr ""
+
+msgid "2FA status"
+msgstr ""
+
+msgid "2FA status legend"
+msgstr ""
+
+msgid "2FA is enabled"
+msgstr ""
+
+msgid "2FA disabled"
+msgstr ""
+
+msgid "2fa is enabled, but secret is not set. User will be given new secret upon first login"
+msgstr ""
+
+msgid "Reset secret"
+msgstr ""
+
+msgid "2FA name"
+msgstr ""
+
+msgid "Name for 2fa application that will be displayed"
+msgstr ""
+
+msgid "2FA length"
+msgstr ""
+
+msgid "Length of 2FA secret (16 to 32)"
+msgstr ""
+
+msgid "2FA user change"
+msgstr ""
+
+msgid "Enabled, not activated"
+msgstr ""
+
+msgid "Can users change 2fa settings for their account"
+msgstr ""
+
+msgid "Apply to all users"
+msgstr ""
+
+msgid "Force all users to use 2fa on next login or disable 2fa for all users"
+msgstr ""
+
+
+
+
+### Password policy ###
+
+msgid "phpIPAM password policy settings"
+msgstr ""
+
+msgid "Here you can set password policy for user authentication"
+msgstr ""
+
+msgid "Minimum length"
+msgstr ""
+
+msgid "Minimum password length"
+msgstr ""
+
+msgid "Maximum length"
+msgstr ""
+
+msgid "Maximum password length"
+msgstr ""
+
+msgid "Minimum numbers"
+msgstr ""
+
+msgid "Minumum number of numbers"
+msgstr ""
+
+msgid "Minimum letters"
+msgstr ""
+
+msgid "Minumum number of letters"
+msgstr ""
+
+msgid "Minimum lowercase letter"
+msgstr ""
+
+msgid "Minumum number of lowercase letters"
+msgstr ""
+
+msgid "Minimum uppercase letter"
+msgstr ""
+
+msgid "Minumum number of uppercase letters"
+msgstr ""
+
+msgid "Minimum symbols"
+msgstr ""
+
+msgid "Minumum number of symbols"
+msgstr ""
+
+msgid "Maximum symbols"
+msgstr ""
+
+msgid "Maximum number of symbols"
+msgstr ""
+
+msgid "Symbols"
+msgstr ""
+
+msgid "List of allowed symbols. csv separated."
+msgstr ""
+
+msgid "Enforce"
+msgstr ""
+
+msgid "Require all users to change password upon next login."
+msgstr ""
+
+
+
+
+
+### Circuits ####
+
+msgid "Circuit options"
+msgstr ""
+
+msgid "Physical Circuits"
+msgstr ""
+
+msgid "Logical Circuits"
+msgstr ""
+
+msgid "Circuit providers"
+msgstr ""
+
+msgid "Circuit map"
+msgstr ""
+
+msgid "Options"
+msgstr ""
+
+msgid "Point A"
+msgstr ""
+
+msgid "Point B"
+msgstr ""
+
+msgid "Provider"
+msgstr ""
+
+msgid "Circuit type"
+msgstr ""
+
+msgid "Capacity"
+msgstr ""
+
+msgid "Purpose"
+msgstr ""
+
+msgid "Circuit count"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Logical circuit physical members"
+msgstr ""
+
+msgid "Available physical circuits"
+msgstr ""
+
+
+
+### Misc 1.4 ###
+
+msgid "Theme"
+msgstr ""
+
+msgid "Select UI theme"
+msgstr ""
+
+msgid "dark"
+msgstr ""
+
+msgid "white"
+msgstr ""
+
+msgid "Compress text in top menu"
+msgstr ""
+
+msgid "Bandwidth calculator"
+msgstr ""
+
+msgid "TCP Window size"
+msgstr ""
+
+msgid "Delay"
+msgstr ""
+
+msgid "Filesize"
+msgstr ""
+
+msgid "Transfer time"
+msgstr ""
+
+msgid "IP Request"
+msgstr ""
+
+msgid "Active IP address requests."
+msgstr ""
+
+msgid "Enable Firewall Zones"
+msgstr ""
+
+msgid "Enable or disable firewall zone management module"
+msgstr ""
+
+msgid "Allow duplicate vlans"
+msgstr ""
+
+msgid "Allow duplicated vlans inside L2 domain"
+msgstr ""
+
+msgid "Decode MAC vendor"
+msgstr ""
+
+msgid "Decode MAC address vendor for addresses"
+msgstr ""
+
+msgid "Module permissions"
+msgstr ""
+
+msgid "Scan agents"
+msgstr ""
+
+
+### 1.6.1
+
+msgid "passkey"
+msgstr ""
+
+msgid "Passkey"
+msgstr ""
+
+msgid "2fa account status"
+msgstr ""
+
+msgid "2fa disabled"
+msgstr ""
+
+msgid "2fa enabled"
+msgstr ""
+
+msgid "2fa status"
+msgstr ""
+
+msgid "Account"
+msgstr ""
+
+msgid "Check passkey you want to remove"
+msgstr ""
+
+msgid "Details for your preferred authenticator application are below. Please write down your details, otherwise you will not be able to login to phpipam"
+msgstr ""
+
+msgid "Disable 2fa for user"
+msgstr ""
+
+msgid "Enable Passkeys"
+msgstr ""
+
+msgid "Enable Vaults"
+msgstr ""
+
+msgid "Enable Vaults for storing encrypted information"
+msgstr ""
+
+msgid "Enable passkeys for passwordless login"
+msgstr ""
+
+msgid "failed"
+msgstr ""
+
+msgid "Failed passkey login"
+msgstr ""
+
+msgid "has logged out"
+msgstr ""
+
+msgid "Here you can change settings for two-factor authentication and get your 2fa secret."
+msgstr ""
+
+msgid "Invalid theme"
+msgstr ""
+
+msgid "logged in"
+msgstr ""
+
+msgid "Login with a passkey"
+msgstr ""
+
+msgid "Only passkey authentication is possible for this account"
+msgstr ""
+
+msgid "Passkey login only"
+msgstr ""
+
+msgid "Passkey only"
+msgstr ""
+
+msgid "Passkey required for login"
+msgstr ""
+
+msgid "Passkeys"
+msgstr ""
+
+msgid "Select to only allow account login with passkey"
+msgstr ""
+
+msgid "successful"
+msgstr ""
+
+msgid "There are no passkeys set for user. Resetting passkey login only to false."
+msgstr ""
+
+msgid "Two-factor authentication"
+msgstr ""
+
+msgid "User account is disabled"
+msgstr ""
+
+msgid "User permissions for phpipam modules"
+msgstr ""
+
+msgid "You can also scan following QR code with your preferred authenticator application"
+msgstr ""
+
+msgid "You can login to your account with normal authentication method only untill you create passkeys."
+msgstr ""
+
+msgid "You can only login to your account using passkeys"
+msgstr ""
+
+msgid "Passwordless authentication"
+msgstr ""
+
+msgid "Account details"
+msgstr ""
+
+msgid "You can login to your account with with passkeys only"
+msgstr ""
+
+msgid "This can be changed under Account details tab"
+msgstr ""
+
+msgid "Your passkeys"
+msgstr ""
+
+msgid "Added on"
+msgstr ""
+
+msgid "Last used"
+msgstr ""
+
+msgid "You authenticated with this passkey"
+msgstr ""
+
+msgid "Add a passkey"
+msgstr ""
+
+msgid "Rename"
+msgstr ""
+
+msgid "Here you can manage passkey authentication for your account"
+msgstr ""
+
+msgid "Passkeys are a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN"
+msgstr ""
+
+msgid "Name your passkey"
+msgstr ""
+
+msgid "Duplicates"
+msgstr ""
+
+msgid "Duplicated subnets"
+msgstr ""
+
+msgid "Duplicated hosts"
+msgstr ""
+
+msgid "No duplicate subnets found"
+msgstr ""
+
+msgid "Routing"
+msgstr ""
+
+msgid "BGP routing"
+msgstr ""
+
+msgid "Add peer"
+msgstr ""
+
+msgid "Peer name"
+msgstr ""
+
+msgid "Peer AS"
+msgstr ""
+
+msgid "Local AS"
+msgstr ""
+
+msgid "Peer address"
+msgstr ""
+
+msgid "Local address"
+msgstr ""
+
+msgid "Local Address"
+msgstr ""
+
+msgid "BGP type"
+msgstr ""
diff --git a/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.mo b/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.mo
index ab67e6fac36132a88d8058024c98bdc57a93d8d8..596e7f2a78d51cec9046a242b30d68a721557d5b 100644
Binary files a/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.mo and b/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.mo differ
diff --git a/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.po b/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.po
index 9e4b1e59dfe12462e6317cafb1de14e022dc23b1..9c751215844acbffb3974590ec1dada8c939397e 100644
--- a/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.po
+++ b/functions/locale/sl_SI.UTF-8/LC_MESSAGES/phpipam.po
@@ -8361,3 +8361,191 @@ msgstr "Pravice modulov"
 msgid "Scan agents"
 msgstr "Agenti skeniranja"
 
+
+### 1.6.1
+
+msgid "passkey"
+msgstr "prijavni ključ"
+
+msgid "Passkey"
+msgstr "prijavni ključ"
+
+msgid "2fa account status"
+msgstr "Status 2fa"
+
+msgid "2fa disabled"
+msgstr "2fa izklopljena"
+
+msgid "2fa enabled"
+msgstr "2fa vklopljena"
+
+msgid "2fa status"
+msgstr "Status 2fa"
+
+msgid "Account"
+msgstr "Račun"
+
+msgid "Check passkey you want to remove"
+msgstr "Izperite prijavni ključ ki ga želite odstraniti"
+
+msgid "Details for your preferred authenticator application are below. Please write down your details, otherwise you will not be able to login to phpipam"
+msgstr ""
+
+msgid "Disable 2fa for user"
+msgstr "Onemogoči 2fa za uporabnika"
+
+msgid "Enable Passkeys"
+msgstr "Omogočite prijavne ključe"
+
+msgid "Enable Vaults"
+msgstr "Omogoċite trezorje"
+
+msgid "Enable Vaults for storing encrypted information"
+msgstr "Omogočite trezorje za varno hrambo informacij"
+
+msgid "Enable passkeys for passwordless login"
+msgstr "Omogočite prijavo brez gesel z uporabo passkey prijavnimi ključi"
+
+msgid "failed"
+msgstr "nauspešna"
+
+msgid "Failed passkey login"
+msgstr "Napaka pri prijavi s prijavnim ključem"
+
+msgid "has logged out"
+msgstr "se je odjavil"
+
+msgid "Here you can change settings for two-factor authentication and get your 2fa secret."
+msgstr "Tukaj lahko spremenite nastavitve za dvofaktorsko avtentikacijo in pridobite vaš 2fa skrivni ključ."
+
+msgid "Invalid theme"
+msgstr "Neveljavna tema"
+
+msgid "logged in"
+msgstr "prijavljen"
+
+msgid "Login with a passkey"
+msgstr "Prijava s prijavnim ključem"
+
+msgid "Only passkey authentication is possible for this account"
+msgstr "Prijava v ta račun je mogoča samo s prijavnimi ključi passkey"
+
+msgid "Passkey login only"
+msgstr "Prijava samo s prijavnimi ključi"
+
+msgid "Passkey only"
+msgstr "Samo prijavni ključi"
+
+msgid "Passkey required for login"
+msgstr "Prijavni ključ je za zahtevan za prijavo"
+
+msgid "Passkeys"
+msgstr "Prijavni ključi"
+
+msgid "Select to only allow account login with passkey"
+msgstr "Izberite to opcijo, da dovolite prijavo v račun samo s prijavnimi ključi"
+
+msgid "successful"
+msgstr "uspešna"
+
+msgid "There are no passkeys set for user. Resetting passkey login only to false."
+msgstr "Uporabnik nima nastavljenih prijavnih ključev."
+
+msgid "Two-factor authentication"
+msgstr "Dvofaktorska avtentikacija"
+
+msgid "User account is disabled"
+msgstr "Uporabniški račun je onemogočen"
+
+msgid "User permissions for phpipam modules"
+msgstr "Uporabniške pravice za phpipam module"
+
+msgid "You can also scan following QR code with your preferred authenticator application"
+msgstr "Spodnjo QR kodo lahko skenirate s svojo applikacijo za upravljanje avtentikacij"
+
+msgid "You can login to your account with normal authentication method only untill you create passkeys."
+msgstr "V svoj račun se lahko prijavite z običajno metodo, dokler ne ustvarite prijavnih ključev."
+
+msgid "You can only login to your account using passkeys"
+msgstr "V račun se lahko prijavite samo s prijavnimi ključi"
+
+msgid "Passwordless authentication"
+msgstr "Prijava brez gesel"
+
+msgid "Account details"
+msgstr "Podatki o računu"
+
+msgid "You can login to your account with with passkeys only"
+msgstr "V račun se lahko prijavite samo s prijavnimi ključi"
+
+msgid "This can be changed under Account details tab"
+msgstr "To lahko spremenite v zavihku Podatki o računu"
+
+msgid "Your passkeys"
+msgstr "Vaši prijavni ključi"
+
+msgid "Added on"
+msgstr "Dodan"
+
+msgid "Last used"
+msgstr "Zadnjič uporabljen"
+
+msgid "You authenticated with this passkey"
+msgstr "Prijavili ste se s tem ključem"
+
+msgid "Add a passkey"
+msgstr "Dodaj prijavni ključ"
+
+msgid "Rename"
+msgstr "Preimenuj"
+
+msgid "Here you can manage passkey authentication for your account"
+msgstr "Tukaj lahko urejate avtentikacijo s prijavnimi ključi za vaš račun"
+
+msgid "Passkeys are a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN"
+msgstr "Prijavni ključi so zamenjava za gesla, ki preverijo vašo identiteto s pomočjo dotika, prepoznave obraza, PIN naprave ali z geslom naprave"
+
+msgid "Name your passkey"
+msgstr "Poimenujte prijavni ključ"
+
+msgid "Duplicates"
+msgstr "Duplikati"
+
+msgid "Duplicated subnets"
+msgstr "Podvojena omreżja"
+
+msgid "Duplicated hosts"
+msgstr "Podvojeni IP naslovi"
+
+msgid "No duplicate subnets found"
+msgstr "Ni podvojenih omrežij"
+
+msgid "Routing"
+msgstr "Usmerjevalni protokoli"
+
+msgid "BGP routing"
+msgstr "BGP usmerjanje"
+
+msgid "Add peer"
+msgstr "Dodaj povezavo"
+
+msgid "Peer name"
+msgstr "Ime partnerja"
+
+msgid "Peer AS"
+msgstr "AS partnerja"
+
+msgid "Local AS"
+msgstr "Lokalni AS"
+
+msgid "Peer address"
+msgstr "Naslov partnerja"
+
+msgid "Local address"
+msgstr "Lokalni naslov"
+
+msgid "Local Address"
+msgstr "Lokalni naslov"
+
+msgid "BGP type"
+msgstr "tip BGP povezave"
diff --git a/functions/scripts/sync_ad_groups.php b/functions/scripts/sync_ad_groups.php
new file mode 100755
index 0000000000000000000000000000000000000000..8e04bcaf8591ea5e1b0171e88f8ca2287bf62423
--- /dev/null
+++ b/functions/scripts/sync_ad_groups.php
@@ -0,0 +1,21 @@
+<?php
+
+
+/**
+ *
+ * This script will sync phpipam groups with AD groups
+ *
+ *
+ *
+ *
+ */
+
+// functions
+require_once( dirname(__FILE__) . '/../functions.php' );
+
+// AD sync
+$Database = new Database_PDO;
+
+$AD_sync  = new AD_user_sync ($Database);
+$AD_sync->set_debug (true);
+
diff --git a/functions/upgrade_queries/upgrade_queries_1.6.php b/functions/upgrade_queries/upgrade_queries_1.6.php
index 84cd23587429b99b8263b362b6f02c60cdc27c01..289feed5a8687a0508b6d2fc4b4189975531d87a 100644
--- a/functions/upgrade_queries/upgrade_queries_1.6.php
+++ b/functions/upgrade_queries/upgrade_queries_1.6.php
@@ -5,4 +5,27 @@
 #
 $upgrade_queries["1.6.39"]   = [];
 $upgrade_queries["1.6.39"][] = "-- Version update";
-$upgrade_queries["1.6.39"][] = "UPDATE `settings` set `version` = '1.6';";
\ No newline at end of file
+$upgrade_queries["1.6.39"][] = "UPDATE `settings` set `version` = '1.6';";
+
+// passkeys
+$upgrade_queries["1.6.40"]   = [];
+$upgrade_queries["1.6.40"][] = "-- Database version bump";
+$upgrade_queries["1.6.40"][] = "UPDATE `settings` set `dbversion` = '40';";
+// add passkey support to settings
+$upgrade_queries["1.6.40"][] = "ALTER TABLE `settings` ADD `passkeys` TINYINT(1)  NULL  DEFAULT '0'  AFTER `2fa_userchange`;";
+// allow passkey login only
+$upgrade_queries["1.6.40"][] = "ALTER TABLE `users` ADD `passkey_only` TINYINT(1)  NOT NULL  DEFAULT '0'  AFTER `authMethod`;";
+// passkey table
+$upgrade_queries["1.6.40"][] = "CREATE TABLE `passkeys` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `credentialId` text NOT NULL,
+  `keyId` text NOT NULL,
+  `credential` text NOT NULL,
+  `comment` text,
+  `created` timestamp NULL DEFAULT NULL,
+  `used` timestamp NULL DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
\ No newline at end of file
diff --git a/functions/vendor/autoload.php b/functions/vendor/autoload.php
new file mode 100644
index 0000000000000000000000000000000000000000..29e50e81ec03c5c718a61ca9c4bf9d9283fe935b
--- /dev/null
+++ b/functions/vendor/autoload.php
@@ -0,0 +1,25 @@
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    trigger_error(
+        $err,
+        E_USER_ERROR
+    );
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit7bf730768732814406175c12775f3390::getLoader();
diff --git a/functions/vendor/composer/ClassLoader.php b/functions/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..7824d8f7eafe8db890975f0fa2dfab31435900da
--- /dev/null
+++ b/functions/vendor/composer/ClassLoader.php
@@ -0,0 +1,579 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    https://www.php-fig.org/psr/psr-0/
+ * @see    https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
+    private $vendorDir;
+
+    // PSR-4
+    /**
+     * @var array<string, array<string, int>>
+     */
+    private $prefixLengthsPsr4 = array();
+    /**
+     * @var array<string, list<string>>
+     */
+    private $prefixDirsPsr4 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    /**
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
+     */
+    private $prefixesPsr0 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr0 = array();
+
+    /** @var bool */
+    private $useIncludePath = false;
+
+    /**
+     * @var array<string, string>
+     */
+    private $classMap = array();
+
+    /** @var bool */
+    private $classMapAuthoritative = false;
+
+    /**
+     * @var array<string, bool>
+     */
+    private $missingClasses = array();
+
+    /** @var string|null */
+    private $apcuPrefix;
+
+    /**
+     * @var array<string, self>
+     */
+    private static $registeredLoaders = array();
+
+    /**
+     * @param string|null $vendorDir
+     */
+    public function __construct($vendorDir = null)
+    {
+        $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+        }
+
+        return array();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    /**
+     * @return array<string, string> Array of classname => path
+     */
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array<string, string> $classMap Class to filename map
+     *
+     * @return void
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @return void
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
+     *
+     * @return void
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     *
+     * @return void
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     *
+     * @return void
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     *
+     * @return void
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     *
+     * @return void
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+        if (null === $this->vendorDir) {
+            return;
+        }
+
+        if ($prepend) {
+            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+        } else {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+            self::$registeredLoaders[$this->vendorDir] = $this;
+        }
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     *
+     * @return void
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+
+        if (null !== $this->vendorDir) {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+        }
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return true|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            $includeFile = self::$includeFile;
+            $includeFile($file);
+
+            return true;
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    /**
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     *
+     * @return array<string, self>
+     */
+    public static function getRegisteredLoaders()
+    {
+        return self::$registeredLoaders;
+    }
+
+    /**
+     * @param  string       $class
+     * @param  string       $ext
+     * @return string|false
+     */
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
+}
diff --git a/functions/vendor/composer/InstalledVersions.php b/functions/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000000000000000000000000000000000000..51e734a774b3ed9ca110a921cb40a74f8c7905c2
--- /dev/null
+++ b/functions/vendor/composer/InstalledVersions.php
@@ -0,0 +1,359 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+    /**
+     * @var mixed[]|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     */
+    private static $installed;
+
+    /**
+     * @var bool|null
+     */
+    private static $canGetVendors;
+
+    /**
+     * @var array[]
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static $installedByVendor = array();
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        $packages = array();
+        foreach (self::getInstalled() as $installed) {
+            $packages[] = array_keys($installed['versions']);
+        }
+
+        if (1 === \count($packages)) {
+            return $packages[0];
+        }
+
+        return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+    }
+
+    /**
+     * Returns a list of all package names with a specific type e.g. 'library'
+     *
+     * @param  string   $type
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackagesByType($type)
+    {
+        $packagesByType = array();
+
+        foreach (self::getInstalled() as $installed) {
+            foreach ($installed['versions'] as $name => $package) {
+                if (isset($package['type']) && $package['type'] === $type) {
+                    $packagesByType[] = $name;
+                }
+            }
+        }
+
+        return $packagesByType;
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @param  bool   $includeDevRequirements
+     * @return bool
+     */
+    public static function isInstalled($packageName, $includeDevRequirements = true)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (isset($installed['versions'][$packageName])) {
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param  string        $packageName
+     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints((string) $constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            $ranges = array();
+            if (isset($installed['versions'][$packageName]['pretty_version'])) {
+                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+            }
+            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+            }
+            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+            }
+            if (array_key_exists('provided', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+            }
+
+            return implode(' || ', $ranges);
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['pretty_version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['reference'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['reference'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+     */
+    public static function getInstallPath($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     */
+    public static function getRootPackage()
+    {
+        $installed = self::getInstalled();
+
+        return $installed[0]['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+     * @return array[]
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     */
+    public static function getRawData()
+    {
+        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                self::$installed = include __DIR__ . '/installed.php';
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        return self::$installed;
+    }
+
+    /**
+     * Returns the raw data of all installed.php which are currently loaded for custom implementations
+     *
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    public static function getAllRawData()
+    {
+        return self::getInstalled();
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+        self::$installedByVendor = array();
+    }
+
+    /**
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static function getInstalled()
+    {
+        if (null === self::$canGetVendors) {
+            self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+        }
+
+        $installed = array();
+
+        if (self::$canGetVendors) {
+            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                if (isset(self::$installedByVendor[$vendorDir])) {
+                    $installed[] = self::$installedByVendor[$vendorDir];
+                } elseif (is_file($vendorDir.'/composer/installed.php')) {
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
+                    if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+                        self::$installed = $installed[count($installed) - 1];
+                    }
+                }
+            }
+        }
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        if (self::$installed !== array()) {
+            $installed[] = self::$installed;
+        }
+
+        return $installed;
+    }
+}
diff --git a/functions/vendor/composer/LICENSE b/functions/vendor/composer/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f27399a042d95c4708af3a8c74d35d338763cf8f
--- /dev/null
+++ b/functions/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/functions/vendor/composer/autoload_classmap.php b/functions/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000000000000000000000000000000000000..0fb0a2c194b8590999a5ed79e357d4a9c1e9d8b8
--- /dev/null
+++ b/functions/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/functions/vendor/composer/autoload_namespaces.php b/functions/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000000000000000000000000000000000000..15a2ff3ad6d8d6ea2b6b1f9552c62d745ffc9bf4
--- /dev/null
+++ b/functions/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/functions/vendor/composer/autoload_psr4.php b/functions/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000000000000000000000000000000000000..edcada0d62ca96e24fe9c2d889d7118e49bd61f3
--- /dev/null
+++ b/functions/vendor/composer/autoload_psr4.php
@@ -0,0 +1,11 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Firehed\\WebAuthn\\' => array($vendorDir . '/firehed/webauthn/src'),
+    'Firehed\\CBOR\\' => array($vendorDir . '/firehed/cbor/src'),
+);
diff --git a/functions/vendor/composer/autoload_real.php b/functions/vendor/composer/autoload_real.php
new file mode 100644
index 0000000000000000000000000000000000000000..53cd7ae35679adfc2a5ffb871c3c88f3776d270b
--- /dev/null
+++ b/functions/vendor/composer/autoload_real.php
@@ -0,0 +1,38 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit7bf730768732814406175c12775f3390
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        require __DIR__ . '/platform_check.php';
+
+        spl_autoload_register(array('ComposerAutoloaderInit7bf730768732814406175c12775f3390', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+        spl_autoload_unregister(array('ComposerAutoloaderInit7bf730768732814406175c12775f3390', 'loadClassLoader'));
+
+        require __DIR__ . '/autoload_static.php';
+        call_user_func(\Composer\Autoload\ComposerStaticInit7bf730768732814406175c12775f3390::getInitializer($loader));
+
+        $loader->register(true);
+
+        return $loader;
+    }
+}
diff --git a/functions/vendor/composer/autoload_static.php b/functions/vendor/composer/autoload_static.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d75bd05a3014d03e244899d4bc67a651b39d35d
--- /dev/null
+++ b/functions/vendor/composer/autoload_static.php
@@ -0,0 +1,41 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInit7bf730768732814406175c12775f3390
+{
+    public static $prefixLengthsPsr4 = array (
+        'F' => 
+        array (
+            'Firehed\\WebAuthn\\' => 17,
+            'Firehed\\CBOR\\' => 13,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'Firehed\\WebAuthn\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/firehed/webauthn/src',
+        ),
+        'Firehed\\CBOR\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/firehed/cbor/src',
+        ),
+    );
+
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInit7bf730768732814406175c12775f3390::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit7bf730768732814406175c12775f3390::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit7bf730768732814406175c12775f3390::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}
diff --git a/functions/vendor/composer/installed.json b/functions/vendor/composer/installed.json
new file mode 100644
index 0000000000000000000000000000000000000000..4c86530bf0887706356c4c32d94ee82ceaeed756
--- /dev/null
+++ b/functions/vendor/composer/installed.json
@@ -0,0 +1,113 @@
+{
+    "packages": [
+        {
+            "name": "firehed/cbor",
+            "version": "0.1.0",
+            "version_normalized": "0.1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Firehed/cbor-php.git",
+                "reference": "eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Firehed/cbor-php/zipball/eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80",
+                "reference": "eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80",
+                "shasum": ""
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.1"
+            },
+            "suggest": {
+                "ext-bcmath": "Enables parsing of very large values"
+            },
+            "time": "2019-05-14T06:31:13+00:00",
+            "type": "library",
+            "installation-source": "source",
+            "autoload": {
+                "psr-4": {
+                    "Firehed\\CBOR\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eric Stern",
+                    "email": "eric@ericstern.com"
+                }
+            ],
+            "description": "CBOR decoder",
+            "homepage": "https://github.com/Firehed/CBOR",
+            "keywords": [
+                "cbor"
+            ],
+            "support": {
+                "issues": "https://github.com/Firehed/cbor-php/issues",
+                "source": "https://github.com/Firehed/cbor-php/tree/master"
+            },
+            "install-path": "../firehed/cbor"
+        },
+        {
+            "name": "firehed/webauthn",
+            "version": "dev-main",
+            "version_normalized": "dev-main",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Firehed/webauthn-php.git",
+                "reference": "267d04a6d2926d9ab6d7630fb86a92410eb6b36c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Firehed/webauthn-php/zipball/267d04a6d2926d9ab6d7630fb86a92410eb6b36c",
+                "reference": "267d04a6d2926d9ab6d7630fb86a92410eb6b36c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-hash": "*",
+                "ext-openssl": "*",
+                "firehed/cbor": "^0.1.0",
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "maglnet/composer-require-checker": "^4.1",
+                "mheap/phpunit-github-actions-printer": "^1.5",
+                "nikic/php-parser": "^4.14",
+                "phpstan/phpstan": "^1.0",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpstan/phpstan-strict-rules": "^1.0",
+                "phpunit/phpunit": "^9.3",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "time": "2023-11-16T23:07:44+00:00",
+            "default-branch": true,
+            "type": "library",
+            "installation-source": "source",
+            "autoload": {
+                "psr-4": {
+                    "Firehed\\WebAuthn\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eric Stern",
+                    "email": "eric@ericstern.com"
+                }
+            ],
+            "description": "Web Authentication",
+            "support": {
+                "issues": "https://github.com/Firehed/webauthn-php/issues",
+                "source": "https://github.com/Firehed/webauthn-php/tree/main"
+            },
+            "install-path": "../firehed/webauthn"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": []
+}
diff --git a/functions/vendor/composer/installed.php b/functions/vendor/composer/installed.php
new file mode 100644
index 0000000000000000000000000000000000000000..384a408b191984ae34c0c83b78c0cc5cd0f525f7
--- /dev/null
+++ b/functions/vendor/composer/installed.php
@@ -0,0 +1,43 @@
+<?php return array(
+    'root' => array(
+        'name' => '__root__',
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'reference' => 'e75387138147905ffd3b12cafffdc10f5388d948',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'dev' => true,
+    ),
+    'versions' => array(
+        '__root__' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'reference' => 'e75387138147905ffd3b12cafffdc10f5388d948',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'firehed/cbor' => array(
+            'pretty_version' => '0.1.0',
+            'version' => '0.1.0.0',
+            'reference' => 'eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../firehed/cbor',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'firehed/webauthn' => array(
+            'pretty_version' => 'dev-main',
+            'version' => 'dev-main',
+            'reference' => '267d04a6d2926d9ab6d7630fb86a92410eb6b36c',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../firehed/webauthn',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'dev_requirement' => false,
+        ),
+    ),
+);
diff --git a/functions/vendor/composer/platform_check.php b/functions/vendor/composer/platform_check.php
new file mode 100644
index 0000000000000000000000000000000000000000..4c3a5d68f144c5aff4a1f9e2fcd2d4bc3be133b5
--- /dev/null
+++ b/functions/vendor/composer/platform_check.php
@@ -0,0 +1,26 @@
+<?php
+
+// platform_check.php @generated by Composer
+
+$issues = array();
+
+if (!(PHP_VERSION_ID >= 80100)) {
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+        } elseif (!headers_sent()) {
+            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+        }
+    }
+    trigger_error(
+        'Composer detected issues in your platform: ' . implode(' ', $issues),
+        E_USER_ERROR
+    );
+}
diff --git a/functions/vendor/firehed/cbor b/functions/vendor/firehed/cbor
new file mode 160000
index 0000000000000000000000000000000000000000..eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80
--- /dev/null
+++ b/functions/vendor/firehed/cbor
@@ -0,0 +1 @@
+Subproject commit eef67b1b5fdf90a3688fc8d9d13afdaf342c4b80
diff --git a/functions/vendor/firehed/webauthn b/functions/vendor/firehed/webauthn
new file mode 160000
index 0000000000000000000000000000000000000000..267d04a6d2926d9ab6d7630fb86a92410eb6b36c
--- /dev/null
+++ b/functions/vendor/firehed/webauthn
@@ -0,0 +1 @@
+Subproject commit 267d04a6d2926d9ab6d7630fb86a92410eb6b36c
diff --git a/functions/version.php b/functions/version.php
index 2f8a7b6de348fef4bb9e46fd6278429f2bfdaf03..fca4fa829809b78fa90728e2e077b2c91c34338e 100644
--- a/functions/version.php
+++ b/functions/version.php
@@ -4,7 +4,7 @@ define("VERSION", "1.6");									//decimal release version e.g 1.32
 /* set latest version */
 define("VERSION_VISIBLE", "1.6.0");							//visible version in footer e.g 1.3.2
 /* set latest revision */
-define("REVISION", "001");									//increment on static content changes (js/css) or point releases to avoid caching issues
+define("REVISION", "002");									//increment on static content changes (js/css) or point releases to avoid caching issues
 /* set last possible upgrade */
 define("LAST_POSSIBLE", "1.19");							//minimum required version to be able to upgrade
 /* set published - hide dbversion in footer */
diff --git a/js/login.js b/js/login.js
index ba26cebe8139d9142b51c4e2b0a6ebddaa6a499e..2b18c75fc2a9a663a9b8aa7c7cda5a1d179c6dce 100755
--- a/js/login.js
+++ b/js/login.js
@@ -5,6 +5,24 @@
  *
  */
 
+/*  loading spinner functions
+*******************************/
+function showSpinner(hide_res = true) {
+    $('div.loading').show();
+    $('input[type=submit]').addClass("disabled");
+    $('button.passkey_login').addClass("disabled");
+    if (hide_res) {
+        $('#loginCheckPasskeys').fadeOut('fast');
+        $('#loginCheck').fadeOut('fast');
+    }
+}
+function hideSpinner() {
+    $('div.loading').fadeOut('fast');
+    $('input[type=submit]').removeClass("disabled");
+    $('button.passkey_login').removeClass("disabled");
+}
+
+
 
 $(document).ready(function() {
 
@@ -14,14 +32,7 @@ $('div.jqueryError').hide();
 $('div.loading').hide();
 
 
-/*	loading spinner functions
-*******************************/
-function showSpinner() {
-    $('div.loading').show();
-}
-function hideSpinner() {
-    $('div.loading').fadeOut('fast');
-}
+
 
 /*	Login redirect function if success
 ****************************************/
@@ -40,13 +51,12 @@ $('form#login').submit(function() {
 
     var logindata = $(this).serialize();
 
-    $('div#loginCheck').hide();
-    //post to check form
+    // post to check form
     $.post('app/login/login_check.php', logindata, function(data) {
         $('div#loginCheck').html(data).fadeIn('fast');
         //reload after 2 seconds if succeeded!
         if(data.search("alert alert-success") != -1) {
-            showSpinner();
+            showSpinner(false);
             //search for redirect
             if($('form#login input#phpipamredirect').length > 0) { setTimeout(function (){window.location=$('form#login input#phpipamredirect').val();}, 1000); }
             else 												 { setTimeout(loginRedirect, 1000);	}
@@ -134,6 +144,93 @@ $(".clearIPrequest").click(function() {
 
 });
 
+
+// passkey login
+$('.passkey_login').click(function() {
+    // execute passkey login
+    startLogin();
+    // prevent submit
+    return false;
+})
+
 });
 
 
+
+function loginRedirect2() {
+    var base = $('.iebase').html();
+    window.location=base;
+}
+
+
+const startLogin = async (e) => {
+
+    // check if browser supports webauthn
+    if (!window.PublicKeyCredential) {
+        return
+    }
+
+    // show login window
+    showSpinner();
+
+    try {
+        // get and parse challenge
+        const challengeReq = await fetch('app/tools/user-menu/passkey_challenge.php')
+        const challengeB64 = await challengeReq.json()
+        const challenge    = atob(challengeB64) // base64-decode
+
+        // Format for WebAuthn API
+        const getOptions = {
+            publicKey: {
+                challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+                allowCredentials: [],
+                mediation: 'conditional',
+            },
+        }
+
+        // Call the WebAuthn browser API and get the response. This may throw, which you
+        // should handle. Example: user cancels or never interacts with the device.
+        const credential = await navigator.credentials.get(getOptions)
+        // console.log(credential)
+
+        // Format the credential to send to the server. This must match the format
+        // handed by the ResponseParser class. The formatting code below can be used
+        // without modification.
+        const dataForResponseParser = {
+            rawId: Array.from(new Uint8Array(credential.rawId)),
+            keyId: credential.id,
+            type: credential.type,
+            authenticatorData: Array.from(new Uint8Array(credential.response.authenticatorData)),
+            clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
+            signature: Array.from(new Uint8Array(credential.response.signature)),
+            userHandle: Array.from(new Uint8Array(credential.response.userHandle)),
+        }
+
+        // Send this to your endpoint - adjust to your needs.
+        const request = new Request('app/login/passkey_login_check.php', {
+            body: JSON.stringify(dataForResponseParser),
+            headers: {
+                'Content-type': 'application/json',
+            },
+            method: 'POST',
+        })
+        const result = await fetch(request)
+
+        // process result by http status returned from passkey_login_check
+        if(result.status==200) {
+            $('#loginCheckPasskeys').html("<div class='alert alert-success'>Passkey authentication successfull</div>").show();
+            setTimeout(loginRedirect2, 1000)
+        }
+        else {
+            $('#loginCheckPasskeys').html("<div class='alert alert-danger'>Passkey authentication failed!</div>").show();
+            console.log(result)
+            hideSpinner()
+        }
+    }
+    // handle throwable error
+    catch(err) {
+        $('#loginCheckPasskeys').html("<div class='alert alert-danger'>Passkey authentication failed!</div>").show();
+        console.log(err)
+        hideSpinner();
+    }
+}
\ No newline at end of file
diff --git a/js/magic.js b/js/magic.js
index 8d6def598158534a497d86a17f7bb433caf93cf2..2e9e70552a00e8157ce2b86c9bddb51c14613967 100755
--- a/js/magic.js
+++ b/js/magic.js
@@ -2418,6 +2418,10 @@ $(document).on("click", "#editVLANdomainsubmit", function() {
     submit_popup_data (".domainEditResult", "app/admin/vlans/edit-domain-result.php", $('form#editVLANdomain').serialize());
 });
 
+/* ---- Show permissions ----- */
+$(document).on("click", ".toggle-module-permissions", function () {
+    $(this).next('div').toggleClass('hidden');
+})
 
 /* ---- VRF ----- */
 //submit form
diff --git a/misc/CHANGELOG b/misc/CHANGELOG
index 76c0aa4f08380ec06ca61397c372cd12bf7ec7bd..f625c7708b4ad936bd7b6917ffe340a54fcc1391 100755
--- a/misc/CHANGELOG
+++ b/misc/CHANGELOG
@@ -1,3 +1,9 @@
+== 1.7.0
+
+    New features:
+    ------------
+    + support for passkeys / passwordless logins;
+
 == 1.6.0
 
     Enhancements, changes: