Added passkey for logins
This commit is contained in:
parent
d90f431925
commit
527cc1d4ab
@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/*
|
||||
@ -38,7 +39,8 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
$this->middleware('guest')
|
||||
->except('logout');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
@ -70,6 +72,7 @@ class LoginController extends Controller
|
||||
if (file_exists('login_note.txt'))
|
||||
$login_note = file_get_contents('login_note.txt');
|
||||
|
||||
return view('auth.login')->with('login_note',$login_note);
|
||||
return view('auth.login')
|
||||
->with('login_note',$login_note);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class AddressMerge extends FormRequest
|
||||
|
||||
public function authorize()
|
||||
{
|
||||
return Gate::allows( 'admin');
|
||||
return Gate::allows('admin');
|
||||
}
|
||||
|
||||
public function rules(Request $request)
|
||||
|
@ -12,7 +12,7 @@ class AreafixRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return Gate::allows( 'admin');
|
||||
return Gate::allows('admin');
|
||||
}
|
||||
|
||||
public function rules(Request $request)
|
||||
|
@ -12,7 +12,7 @@ class DomainRequest extends FormRequest
|
||||
{
|
||||
public function authorize(Domain $o)
|
||||
{
|
||||
return Gate::allows( 'admin',$o);
|
||||
return Gate::allows('admin',$o);
|
||||
}
|
||||
|
||||
public function rules(Request $request)
|
||||
|
@ -10,7 +10,7 @@ class SetupRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return Gate::allows( 'admin');
|
||||
return Gate::allows('admin');
|
||||
}
|
||||
|
||||
public function rules(Request $request)
|
||||
|
@ -7,13 +7,11 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return Gate::allows( 'admin');
|
||||
return Gate::any(['admin','update'],$this->route('o'));
|
||||
}
|
||||
|
||||
public function rules(Request $request)
|
||||
|
@ -57,7 +57,8 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_on' => 'datetime:Y-m-d H:i:s'
|
||||
'last_on' => 'datetime:Y-m-d H:i:s',
|
||||
'passkey' => 'json',
|
||||
];
|
||||
|
||||
/* RELATIONS */
|
||||
|
@ -18,6 +18,7 @@
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/ui": "^4.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"leenooks/passkey": "^0.1.0",
|
||||
"nunomaduro/laravel-console-summary": "^1.9",
|
||||
"rennokki/laravel-eloquent-query-cache": "^3.3",
|
||||
"repat/laravel-job-models": "^0.8",
|
||||
@ -47,6 +48,10 @@
|
||||
}
|
||||
},
|
||||
"repositories": {
|
||||
"passkey": {
|
||||
"type": "vcs",
|
||||
"url": "https://gitea.dege.au/laravel/passkey.git"
|
||||
},
|
||||
"laravel-console-summary": {
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/leenooks/laravel-console-summary"
|
||||
|
88
composer.lock
generated
88
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b9697fb5a34c3ed05cea0ce107b63d3f",
|
||||
"content-hash": "0f0e0e75d37ebcbcbc03996fb1f3e18b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aglipanci/laravel-eloquent-case",
|
||||
@ -1700,6 +1700,51 @@
|
||||
},
|
||||
"time": "2023-05-09T19:47:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lbuchs/webauthn",
|
||||
"version": "v2.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lbuchs/WebAuthn.git",
|
||||
"reference": "e73ff007e8a1099e72e0dbdd9d0884057409fc54"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/e73ff007e8a1099e72e0dbdd9d0884057409fc54",
|
||||
"reference": "e73ff007e8a1099e72e0dbdd9d0884057409fc54",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"lbuchs\\WebAuthn\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lukas Buchs",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple PHP WebAuthn (FIDO2) server library",
|
||||
"homepage": "https://github.com/lbuchs/webauthn",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"webauthn"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lbuchs/WebAuthn/issues",
|
||||
"source": "https://github.com/lbuchs/WebAuthn/tree/v2.1.1"
|
||||
},
|
||||
"time": "2024-01-15T15:46:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/commonmark",
|
||||
"version": "2.4.1",
|
||||
@ -2160,6 +2205,47 @@
|
||||
],
|
||||
"time": "2023-10-17T14:13:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leenooks/passkey",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.dege.au/laravel/passkey.git",
|
||||
"reference": "4872d55ed65863e03102eea1046504e630f80ae2"
|
||||
},
|
||||
"require": {
|
||||
"lbuchs/webauthn": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Leenooks\\Passkey\\PasskeyServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Leenooks\\Passkey\\": "src"
|
||||
}
|
||||
},
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Deon George",
|
||||
"email": "deon@dege.au"
|
||||
}
|
||||
],
|
||||
"description": "Leenooks Laravel Passkey Implementation",
|
||||
"keywords": [
|
||||
"dege",
|
||||
"laravel",
|
||||
"passkey"
|
||||
],
|
||||
"time": "2024-04-25T05:05:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.8.1",
|
||||
|
225
public/passkey/passkey.js
vendored
Normal file
225
public/passkey/passkey.js
vendored
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Passkey Implementation
|
||||
*/
|
||||
let passkey_debug = false;
|
||||
|
||||
/**
|
||||
* Convert a ArrayBuffer to Base64
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {String}
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert RFC 1342-like base64 strings to array buffer
|
||||
* @param {mixed} obj
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function recursiveBase64StrToArrayBuffer(obj) {
|
||||
let prefix = '=?BINARY?B?';
|
||||
let suffix = '?=';
|
||||
if (typeof obj === 'object') {
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
let str = obj[key];
|
||||
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||
str = str.substring(prefix.length, str.length - suffix.length);
|
||||
|
||||
let binary_string = window.atob(str);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
obj[key] = bytes.buffer;
|
||||
}
|
||||
} else {
|
||||
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function passkey_check_browser()
|
||||
{
|
||||
// check browser support
|
||||
if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create))
|
||||
throw new Error('Browser not supported.');
|
||||
|
||||
/*
|
||||
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||||
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||||
// `isConditionalMediationAvailable` means the feature detection is usable.
|
||||
if (window.PublicKeyCredential &&
|
||||
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||||
PublicKeyCredential.isConditionalMediationAvailable) {
|
||||
// Check if user verifying platform authenticator is available.
|
||||
Promise.all([
|
||||
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||||
PublicKeyCredential.isConditionalMediationAvailable(),
|
||||
]).then(results => {
|
||||
if (results.every(r => r === true)) {
|
||||
// Display "Create a new passkey" button
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Browser OK');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/Create a passkey for a user
|
||||
*/
|
||||
async function passkey_register(csrf_token,icon_dom,icon,icon_shell_success,icon_shell_fail)
|
||||
{
|
||||
try {
|
||||
if (! passkey_check_browser())
|
||||
return;
|
||||
|
||||
// Change our icon so that it is obvious we are doing something
|
||||
icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm');
|
||||
|
||||
// Get our arguments
|
||||
var createArgs;
|
||||
$.ajax({
|
||||
url: '/passkey/register',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
async: false,
|
||||
cache: false,
|
||||
success: function(data) {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Get Register Success');
|
||||
|
||||
recursiveBase64StrToArrayBuffer(data);
|
||||
createArgs = data;
|
||||
},
|
||||
error: function(e,status,error) {
|
||||
throw new Error(status || 'Unknown error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
// Create credentials
|
||||
try {
|
||||
const cred = await navigator.credentials.create(createArgs);
|
||||
|
||||
const authenticatorAttestationResponse = {
|
||||
id: cred.id,
|
||||
rawId: arrayBufferToBase64(cred.rawId),
|
||||
transports: cred.response.getTransports ? cred.response.getTransports() : null,
|
||||
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
|
||||
authenticatorAttachment: cred.authenticatorAttachment,
|
||||
_token: csrf_token,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/passkey/check',
|
||||
type: 'POST',
|
||||
data: authenticatorAttestationResponse,
|
||||
cache: false,
|
||||
success: function(data) {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Registration Success');
|
||||
|
||||
icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||||
icon_dom.removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
},
|
||||
error: function(e,status,error) {
|
||||
throw new Error(status || 'Unknown error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (status) {
|
||||
if (passkey_debug)
|
||||
console.log(status || 'Passkey: User Aborted Register');
|
||||
|
||||
// Restore the icon
|
||||
icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
window.alert(err || 'An UNKNOWN error occurred?');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a passkey being presented
|
||||
*/
|
||||
async function passkey_check(csrf_token,redirect)
|
||||
{
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Check User Passkey');
|
||||
|
||||
try {
|
||||
if (! passkey_check_browser())
|
||||
return;
|
||||
|
||||
// Get our arguments
|
||||
var getArgs;
|
||||
$.ajax({
|
||||
url: '/passkey/get',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
async: false,
|
||||
cache: false,
|
||||
success: function(data) {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Get Args Success');
|
||||
|
||||
recursiveBase64StrToArrayBuffer(data);
|
||||
getArgs = data;
|
||||
},
|
||||
error: function(e,status,error) {
|
||||
throw new Error(status || 'Unknown error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
// check credentials with hardware
|
||||
const cred = await navigator.credentials.get(getArgs);
|
||||
|
||||
// create object for transmission to server
|
||||
const authenticatorAttestationResponse = {
|
||||
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
|
||||
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
|
||||
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
|
||||
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null,
|
||||
_token: csrf_token
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/passkey/process',
|
||||
type: 'POST',
|
||||
data: authenticatorAttestationResponse,
|
||||
cache: false,
|
||||
success: function(data) {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Process Success');
|
||||
|
||||
// Direct to the home page
|
||||
window.location.href = (redirect !== undefined) ? redirect : '/';
|
||||
},
|
||||
error: function(e,status,error) {
|
||||
throw new Error(status || 'Unknown error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
window.alert(err || 'An UNKNOWN error occurred?');
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(isset($login_note) AND $login_note)
|
||||
@if(isset($login_note) && $login_note)
|
||||
<div class="row">
|
||||
<div class="col-8 m-auto">
|
||||
<div class="alert alert-info alert-dismissible" role="alert">
|
||||
@ -20,20 +20,21 @@
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 m-auto">
|
||||
<div class="greyframe titledbox shadow0xb0 text-center">
|
||||
<h2 class="cap">Login</h2>
|
||||
<form class="needs-validation" method="post" novalidate>
|
||||
@csrf
|
||||
|
||||
<form class="row g-0 needs-validation" method="post" novalidate>
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-6 m-auto">
|
||||
<div class="greyframe titledbox shadow0xb0 text-center">
|
||||
<h2 class="cap">Login</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-person-badge"></i></span>
|
||||
<input type="text" class="form-control @error('email') is-invalid @enderror" id="email" placeholder="Email" name="email" required autocomplete="email" autofocus>
|
||||
<!-- Conditionally display passkeys in autofill -->
|
||||
<input type="text" class="form-control @error('email') is-invalid @enderror" id="email" placeholder="Email" name="email" required autocomplete="email webauthn" autofocus>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('email')
|
||||
{{ $message }}
|
||||
@ -50,7 +51,7 @@
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-key-fill"></i></span>
|
||||
<input type="password" class="form-control" id="password" placeholder="Password" name="password" required>
|
||||
<input type="password" class="form-control" id="password" placeholder="Password" name="password" autocomplete="new-password" required>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
Your password is required.
|
||||
</span>
|
||||
@ -70,20 +71,29 @@
|
||||
<button type="submit" name="submit" class="btn btn-success float-end">Sign In</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a class="link-danger" href="{{ url('password/reset') }}">Forgot Password</a>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a class="link-danger" href="{{ url('password/reset') }}">Forgot Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a class="link-danger" href="{{ url('register') }}">Register</a>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a class="link-danger" href="{{ url('register') }}">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('page-scripts')
|
||||
<!-- Passkeys -->
|
||||
<script type='text/javascript' src='{{ asset('/passkey/passkey.js') }}'></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
passkey_check('{{ csrf_token() }}','{{ back()->getTargetUrl() }}');
|
||||
</script>
|
||||
@append
|
@ -43,7 +43,7 @@
|
||||
<dl>
|
||||
<dt>Users</dt>
|
||||
<dd><a href="{{ url('user/addedit') }}">Create</a></dd>
|
||||
<dd><a href="{{ url('user') }}">List</a></dd>
|
||||
<dd><a href="{{ url('user/list') }}">List</a></dd>
|
||||
</dl>
|
||||
@endcan
|
||||
@endauth
|
||||
|
@ -32,6 +32,7 @@
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="user-menu-list">
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="{{ url('user/addedit',$user->id) }}">Account Settings</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url('user/system/register') }}">Register/Link System</a></li>
|
||||
@can('admin')
|
||||
<li><a class="dropdown-item @if(preg_match('#^setup#',request()->path()))thispage @endif" href="{{ url('setup') }}">Setup</a></li>
|
||||
|
@ -271,7 +271,7 @@ use App\Classes\Protocol\{Binkp,EMSI,DNS};
|
||||
|
||||
<div class="row pt-5">
|
||||
<div class="col-2">
|
||||
<a href="{{ url('domain') }}" class="btn btn-danger">Cancel</a>
|
||||
<a href="{{ back()->getTargetUrl() }}" class="btn btn-danger">Cancel</a>
|
||||
</div>
|
||||
|
||||
<span class="col-6 mt-auto mx-auto text-center align-bottom">
|
||||
|
@ -1,3 +1,4 @@
|
||||
<!-- $o=User::class -->
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('htmlheader_title')
|
||||
@ -18,7 +19,7 @@
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-tag-fill"></i></span>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror" id="name" placeholder="Name" name="name" value="{{ old('name',$o->name) }}" required @cannot('admin',$o)disabled @endcannot autofocus>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror" id="name" placeholder="Name" name="name" value="{{ old('name',$o->name) }}" autocomplete="name" required @cannot('admin',$o)disabled @endcannot autofocus>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('name')
|
||||
{{ $message }}
|
||||
@ -33,7 +34,7 @@
|
||||
<label for="alias" class="form-label">BBS Alias</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-tag-fill"></i></span>
|
||||
<input type="text" class="form-control @error('alias') is-invalid @enderror" id="alias" placeholder="alias" name="alias" value="{{ old('alias',$o->alias) }}" @cannot('admin',$o)disabled @endcannot>
|
||||
<input type="text" class="form-control @error('alias') is-invalid @enderror" id="alias" placeholder="alias" name="alias" value="{{ old('alias',$o->alias) }}" @cannot('update',$o)disabled @endcannot>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('alias')
|
||||
{{ $message }}
|
||||
@ -43,23 +44,25 @@
|
||||
</div>
|
||||
|
||||
<!-- Forward Netmail -->
|
||||
<div class="col-4">
|
||||
<label for="system_id" class="form-label">Forward Netmails</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-envelope-at-fill"></i></span>
|
||||
<select style="width: 80%;" class="form-select @error('system_id') is-invalid @enderror" id="system_id" name="system_id" required @cannot('admin',$o)disabled @endcannot>
|
||||
<option value=""> </option>
|
||||
@foreach ($o->systems as $oo)
|
||||
<option value="{{ $oo->id }}" @if(old('system_id',$o->system_id)==$oo->id)selected @endif>{{ $oo->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('system_id')
|
||||
{{ $message }}
|
||||
@enderror
|
||||
</span>
|
||||
@can('admin',$o)
|
||||
<div class="col-4">
|
||||
<label for="system_id" class="form-label">Forward Netmails</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-envelope-at-fill"></i></span>
|
||||
<select style="width: 80%;" class="form-select @error('system_id') is-invalid @enderror" id="system_id" name="system_id" required>
|
||||
<option value=""> </option>
|
||||
@foreach ($o->systems as $oo)
|
||||
<option value="{{ $oo->id }}" @if(old('system_id',$o->system_id)==$oo->id)selected @endif>{{ $oo->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('system_id')
|
||||
{{ $message }}
|
||||
@enderror
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@ -67,7 +70,7 @@
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-person-badge"></i></span>
|
||||
<input type="text" class="form-control @error('email') is-invalid @enderror" id="email" placeholder="Email" name="email" value="{{ old('email',$o->email) }}" required @cannot('admin',$o)disabled @endcannot>
|
||||
<input type="text" class="form-control @error('email') is-invalid @enderror" id="email" placeholder="Email" name="email" value="{{ old('email',$o->email) }}" autocomplete="email" required @cannot('update',$o)disabled @endcannot>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('email')
|
||||
{{ $message }}
|
||||
@ -78,37 +81,72 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="bi bi-person-badge"></i></span>
|
||||
<input type="password" class="form-control @error('password') is-invalid @enderror" id="password" placeholder="{{ old('password',$o->password) ? 'Password Unchanged' : 'Password' }}" name="password" value="" @cannot('update',$o)disabled @endcannot>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('password')
|
||||
{{ $message }}
|
||||
@else
|
||||
Password required for login.
|
||||
@enderror
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-1">
|
||||
<label for="active" class="form-label">Active</label>
|
||||
<div class="input-group">
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" class="btn-check" name="active" id="active_yes" value="1" required @cannot('admin',$o)disabled @endcannot @if(old('active',$o->active))checked @endif>
|
||||
<label class="btn btn-outline-success" for="active_yes">Yes</label>
|
||||
|
||||
<input type="radio" class="btn-check btn-danger" name="active" id="active_no" value="0" required @cannot('admin',$o)disabled @endcannot @if(! old('active',$o->active))checked @endif>
|
||||
<label class="btn btn-outline-danger" for="active_no">No</label>
|
||||
@can('ownes',$o)
|
||||
<label for="passkey" class="form-label">Passkey</label>
|
||||
<div class="input-group has-validation">
|
||||
<button class="btn {{ $o->passkey ? 'btn-primary' : 'btn-outline-primary' }}" id="passkey"><i class="bi bi-key"></i></button>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('passkey')
|
||||
{{ $message }}
|
||||
@enderror
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="offset-1 col-1">
|
||||
<label for="admin" class="form-label">Site Admin</label>
|
||||
<div class="input-group">
|
||||
<div class="btn-group" role="group" @if($user->id === $o->id)data-bs-toggle="tooltip" title="You cannot demote yourself" @endif>
|
||||
<input type="radio" class="btn-check" name="admin" id="admin_yes" value="1" required @cannot('admin',$o)disabled @endcannot @if(old('admin',$o->admin))checked @endif>
|
||||
<label class="btn btn-outline-success" for="admin_yes">Yes</label>
|
||||
@can('admin',$o)
|
||||
<div class="col-3">
|
||||
<div class="row p-0">
|
||||
<div class="col-xl-6 col-12">
|
||||
<span class="form-label" style="font-size: 75%; margin-bottom: 1px;">Active</span>
|
||||
<div class="input-group">
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" class="btn-check" name="active" id="active_yes" value="1" required @if(old('active',$o->active))checked @endif>
|
||||
<label class="btn btn-outline-success" for="active_yes">Yes</label>
|
||||
|
||||
<input type="radio" class="btn-check btn-danger" name="admin" id="admin_no" value="0" required @if(($user->id === $o->id) || $user->cannot('admin',$o)) disabled @endif @if(! old('admin',$o->admin))checked @endif>
|
||||
<label class="btn btn-outline-danger" for="admin_no">No</label>
|
||||
<input type="radio" class="btn-check btn-danger" name="active" id="active_no" value="0" required @if(! old('active',$o->active))checked @endif>
|
||||
<label class="btn btn-outline-danger" for="active_no">No</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6 col-12">
|
||||
<span class="form-label" style="font-size: 75%; margin-bottom: 1px;">Site Admin</span>
|
||||
<div class="input-group">
|
||||
<div class="btn-group" role="group" @if($user->id === $o->id)data-bs-toggle="tooltip" title="You cannot demote yourself" @endif>
|
||||
<input type="radio" class="btn-check" name="admin" id="admin_yes" value="1" required @if(old('admin',$o->admin))checked @endif>
|
||||
<label class="btn btn-outline-success" for="admin_yes">Yes</label>
|
||||
|
||||
<input type="radio" class="btn-check btn-danger" name="admin" id="admin_no" value="0" required @if(($user->id === $o->id) || $user->cannot('admin',$o)) disabled @endif @if(! old('admin',$o->admin))checked @endif>
|
||||
<label class="btn btn-outline-danger" for="admin_no">No</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label for="pgp_pubkey" class="form-label">PGP Public Key</label>
|
||||
<textarea class="form-control @error('pgp_pubkey')is-invalid @enderror" rows=3 name="pgp_pubkey" placeholder="PGP Public Key..." @cannot('admin',$o)disabled @endcannot>{{ old('pgp_pubkey',$o->pgp_pubkey) }}</textarea>
|
||||
<textarea class="form-control @error('pgp_pubkey')is-invalid @enderror" rows=3 id="pgp_pubkey" name="pgp_pubkey" placeholder="PGP Public Key..." @cannot('update',$o)disabled @endcannot>{{ old('pgp_pubkey',$o->pgp_pubkey) }}</textarea>
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@error('pgp_pubkey')
|
||||
{{ $message }}
|
||||
@ -119,8 +157,8 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="{{ url('user') }}" class="btn btn-danger">Cancel</a>
|
||||
@can('admin',$o)
|
||||
<a href="{{ back()->getTargetUrl() }}" class="btn btn-danger">Cancel</a>
|
||||
@canany(['admin','update'],$o)
|
||||
<button type="submit" name="submit" class="btn btn-success float-end">@if ($o->exists)Save @else Add @endif</button>
|
||||
@endcan
|
||||
</div>
|
||||
@ -170,8 +208,11 @@
|
||||
@section('page-scripts')
|
||||
@js('select2')
|
||||
|
||||
<!-- Passkeys -->
|
||||
<script type='text/javascript' src='{{ asset('/passkey/passkey.js') }}'></script>
|
||||
|
||||
@if($user->id === $o->id)
|
||||
<script>
|
||||
<script type="text/javascript">
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
@ -182,6 +223,37 @@
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('#system_id').select2();
|
||||
$('#passkey').on('click',function(item) {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Create Click');
|
||||
|
||||
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||||
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||||
// `sConditionalMediationAvailable` means the feature detection is usable.
|
||||
if (window.PublicKeyCredential &&
|
||||
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||||
PublicKeyCredential.isConditionalMediationAvailable) {
|
||||
// Check if user verifying platform authenticator is available.
|
||||
Promise.all([
|
||||
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||||
PublicKeyCredential.isConditionalMediationAvailable(),
|
||||
]).then(results => {
|
||||
if (passkey_debug)
|
||||
console.log('Passkey: Browser Supported');
|
||||
|
||||
if (results.every(r => r === true)) {
|
||||
passkey_register('{{ csrf_token() }}',$(this),'bi-key','btn-primary','{{ $o->passkey ? 'btn-primary' : 'btn-outline-primary' }}');
|
||||
} else {
|
||||
alert('It seems that passkey is NOT supported by your browse (B)');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
alert('It seems that passkey is NOT supported by your browser (A)');
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@append
|
@ -8,7 +8,7 @@
|
||||
<div class="col-12">
|
||||
<h2>System Users</h2>
|
||||
|
||||
<p>This system is aware of the following users @can('admin',(new \App\Models\User))(you can <a href="{{ url('user/addedit') }}">add</a> more)@endcan:</p>
|
||||
<p>This system is aware of the following users (you can <a href="{{ url('user/addedit') }}">add</a> more):</p>
|
||||
<table class="table monotable" id="user">
|
||||
<thead>
|
||||
<tr>
|
@ -56,7 +56,8 @@ Route::middleware(['auth','verified','activeuser'])->group(function () {
|
||||
Route::view('dashboard','dashboard');
|
||||
|
||||
/* ACCOUNT PATHS */
|
||||
Route::view('user/update','user/update');
|
||||
Route::match(['get','post'],'user/addedit/{o?}',[UserController::class,'add_edit'])
|
||||
->where('o','[0-9]+');
|
||||
|
||||
/* DOMAIN PATHS */
|
||||
Route::view('domain','domain.home');
|
||||
@ -146,7 +147,5 @@ Route::middleware(['auth','can:admin'])->group(function () {
|
||||
Route::match(['get','post'],'address/merge/{id}',[SystemController::class,'address_merge']);
|
||||
Route::match(['get','post'],'setup',[HomeController::class,'setup']);
|
||||
|
||||
Route::view('user','user.home');
|
||||
Route::match(['get','post'],'user/addedit/{o?}',[UserController::class,'add_edit'])
|
||||
->where('o','[0-9]+');
|
||||
Route::view('user/list','user.list');
|
||||
});
|
Loading…
Reference in New Issue
Block a user