Work on registration of existing systems to users

This commit is contained in:
Deon George 2022-03-14 22:28:54 +11:00
parent d68307461e
commit 8072f7c5a9
19 changed files with 553 additions and 56 deletions

View File

@ -36,6 +36,7 @@ class Page
$this->logo = new ANSI;
$this->left_box = new Font;
$this->crlf = $crlf;
$this->text = '';
}
public function __get($key)
@ -114,7 +115,8 @@ class Page
*/
public function addText(string $text,bool $right=FALSE)
{
$this->text = $text;
$this->text .= $text;
$this->text_right = $right;
}

View File

@ -7,11 +7,13 @@ use Illuminate\Support\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ViewErrorBag;
use App\Http\Requests\SystemRegister;
use App\Models\{Address,Echoarea,System,SystemZone,Zone};
use App\Models\{Address,Echoarea,Setup,System,SystemZone,Zone};
use App\Notifications\AddressLink;
use App\Rules\{FidoInteger,TwoByteInteger};
class SystemController extends Controller
@ -258,6 +260,8 @@ class SystemController extends Controller
*/
public function add_edit(SystemRegister $request,System $o)
{
$this->authorize('update',$o);
if ($request->post()) {
foreach (['name','location','sysop','hold','phone','address','port','active','method','notes','mailer_type','mailer_address','mailer_port','zt_id'] as $key)
$o->{$key} = $request->post($key);
@ -269,11 +273,9 @@ class SystemController extends Controller
$o->load(['addresses.zone.domain']);
return Gate::check('update',$o)
? view('system.addedit')
return view('system.addedit')
->with('action',$o->exists ? 'update' : 'create')
->with('o',$o)
: redirect()->to('user/system/register');
->with('o',$o);
}
public function api_address(Request $request,System $o): Collection
@ -300,6 +302,42 @@ class SystemController extends Controller
->get();
}
/**
* Identify all the addresses from systems that are not owned by a user
*
* @param Request $request
* @return Collection
*/
public function api_orphan_address(Request $request): Collection
{
$result = collect();
list($zone_id,$host_id,$node_id,$point_id,$domain) = sscanf($request->query('term'),'%d:%d/%d.%d@%s');
# Look for Systems
foreach (Address::select(['addresses.id','systems.name',DB::raw('systems.id AS system_id'),'zones.zone_id','region_id','host_id','node_id','point_id','addresses.zone_id'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->rightjoin('systems',['systems.id'=>'addresses.system_id'])
->when($zone_id || $host_id || $node_id,function($query) use ($zone_id,$host_id,$node_id) {
return $query
->when($zone_id,function($q,$zone_id) { return $q->where('zones.zone_id',$zone_id); })
->where(function($q) use ($host_id) {
return $q
->when($host_id,function($q,$host_id) { return $q->where('region_id',$host_id); })
->when($host_id,function($q,$host_id) { return $q->orWhere('host_id',$host_id); });
})
->when($node_id,function($q,$node_id) { return $q->where('node_id',$node_id); });
})
->orWhere('systems.name','ilike','%'.$request->query('term').'%')
->orderBy('systems.name')
->get() as $o)
{
$result->push(['id'=>$o->id,'name'=>sprintf('%s (%s)',$o->ftn3d,$o->name),'category'=>'Systems']);
}
return $result;
}
/**
* Delete address assigned to a host
*
@ -450,8 +488,26 @@ class SystemController extends Controller
*/
public function system_register(SystemRegister $request)
{
// Step 1, show the user a form to select an existing defined system
if ($request->isMethod('GET'))
return view('user.system.register');
$o = System::findOrNew($request->system_id);
// If the system exists, and we are 'register', we'll start the address claim process
if ($o->exists && $request->action == 'register') {
$validate = Setup::findOrFail(config('app.id'))->system->inMyZones($o->addresses);
// If we have addresses, we'll trigger the routed netmail
if ($validate->count())
Notification::route('netmail',$x=$validate->first())->notify(new AddressLink($x,Auth::user()));
return view('user.system.widget.register_confirm')
->with('validate',$validate)
->with('o',$o);
}
// If the system doesnt exist, we'll create it
if (! $o->exist) {
$o->sysop = Auth::user()->name;

View File

@ -5,8 +5,9 @@ namespace App\Http\Controllers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
use App\Models\{Address,User};
class UserController extends Controller
{
@ -63,6 +64,36 @@ class UserController extends Controller
return view('user.home');
}
public function link(Request $request)
{
if ($request->post()) {
$request->validate([
'address_id'=>'required|exists:addresses,id',
'code'=>'required:string',
]);
$ao = Address::findOrFail($request->address_id);
if ($ao->check_activation(Auth::user(),$request->code)) {
$ao->validated = TRUE;
$ao->save();
$ao->system->users()->save(Auth::user());
return redirect()->to('/');
} else {
$validator = Validator::make([],[]);
$validator->errors()->add(
'code', 'Invalid Code!'
);
return back()->withErrors($validator);
}
}
return view('user.link');
}
public function register()
{
return view('user/system/register');

View File

@ -21,7 +21,7 @@ class SystemRegister extends FormRequest
{
$this->so = System::findOrNew($request->system_id);
return Gate::allows($this->so->exists ? 'update' : 'create',$this->so);
return Gate::allows($this->so->users->count() ? 'update' : 'register',$this->so);
}
/**
@ -31,7 +31,7 @@ class SystemRegister extends FormRequest
*/
public function rules(Request $request)
{
if (! $request->isMethod('post'))
if ((! $request->isMethod('post')) || ($request->action == 'register'))
return [];
if ((! $this->so->exists) && ($request->action == 'create')) {

View File

@ -7,6 +7,7 @@ use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -369,6 +370,41 @@ class Address extends Model
return ($o && $o->system->active) ? $o : NULL;
}
/**
* Create an activation code for this address
*
* @param User $uo
* @return string
*/
public function set_activation(User $uo): string
{
return sprintf('%x:%s',
$this->id,
substr(md5(sprintf('%d:%x',$uo->id,timew($this->updated_at))),0,10)
);
}
/**
* Check the user's activation code for this address is correct
*
* @param User $uo
* @param string $code
* @return bool
*/
public function check_activation(User $uo,string $code): bool
{
try {
Log::info(sprintf('%s:Checking Activation code [%s] invalid for user [%d]',self::LOGKEY,$code,$uo->id));
return ($code == $this->set_activation($uo));
} catch (\Exception $e) {
Log::error(sprintf('%s:! Activation code [%s] invalid for user [%d]',self::LOGKEY,$code,$uo->id));
return FALSE;
}
}
/**
* Netmail waiting to be sent to this system
*

View File

@ -9,13 +9,13 @@ use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Message;
use App\Interfaces\Packet;
use App\Traits\EncodeUTF8;
use App\Traits\{EncodeUTF8,MsgID};
final class Netmail extends Model implements Packet
{
private const LOGKEY = 'MN-';
use SoftDeletes,EncodeUTF8;
use SoftDeletes,EncodeUTF8,MsgID;
private const cast_utf8 = [
'to',

View File

@ -8,7 +8,9 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
/**
* Class Setup
* This class represents our configuration.
*
* Our 'System' is defined by system_id, and from it we can find out our BBS name and addresses.
*
* @package App\Models
* @property Collection nodes
@ -47,37 +49,6 @@ class Setup extends Model
// Our non model attributes and values
private array $internal = [];
public static function product_id(int $c=self::PRODUCT_ID): string
{
return hexstr($c);
}
/* RELATIONS */
public function system()
{
return $this->belongsTo(System::class);
}
/* ATTRIBUTES */
public function getLocationAttribute()
{
return $this->system->location;
}
public function getSysopAttribute()
{
return $this->system->sysop;
}
public function getSystemNameAttribute()
{
return $this->system->name;
}
/* METHODS */
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
@ -143,6 +114,49 @@ class Setup extends Model
}
}
/**
* The Mailer Product ID in hex.
*
* @param int $c
* @return string
* @throws Exception
*/
public static function product_id(int $c=self::PRODUCT_ID): string
{
return hexstr($c);
}
/* RELATIONS */
/**
* The defined system that this setup is valid for
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function system()
{
return $this->belongsTo(System::class);
}
/* ATTRIBUTES */
public function getLocationAttribute()
{
return $this->system->location;
}
public function getSysopAttribute()
{
return $this->system->sysop;
}
public function getSystemNameAttribute()
{
return $this->system->name;
}
/* METHODS */
/* BINKP OPTIONS: BINKP_OPT_* */
public function binkpOptionClear(int $key): void

View File

@ -133,4 +133,20 @@ class System extends Model
return $item->role & $type;
});
}
/**
* Parse the addresses and return which ones are in my zones
*
* @param \Illuminate\Database\Eloquent\Collection $addresses
* @param int $type
* @return Collection
*/
public function inMyZones(Collection $addresses,int $type=(Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT)): Collection
{
$myzones = $this->addresses->pluck('zone_id')->unique();
return $addresses->filter(function($item) use ($myzones,$type) {
return ($item->role & $type) && ($myzones->search($item->zone_id) !== FALSE);
});
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Notifications;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\{ANSI,Fonts\Thin,Page};
use App\Classes\Fonts\Thick;
use App\Classes\FTN\Message;
use App\Models\{Address,Netmail,Setup,User};
class AddressLink extends Notification //implements ShouldQueue
{
private const LOGKEY = 'NAL';
use Queueable;
private Address $ao;
private User $uo;
/**
* Create a netmail to enable a sysop to activate an address.
*
* @param Address $ao
* @param User $uo
*/
public function __construct(Address $ao,User $uo)
{
$this->queue = 'netmail';
$this->ao = $ao;
$this->uo = $uo;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['netmail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return Netmail
* @throws \Exception
*/
public function toNetmail($notifiable): Netmail
{
Log::info(sprintf('%s:Sending a link code for address [%s]',self::LOGKEY,$this->ao->ftn));
$so = Setup::findOrFail(config('app.id'))->system;
$o = new Netmail;
$o->to = $this->ao->system->sysop;
$o->from = Setup::PRODUCT_NAME;
$o->subject = 'Address Link Code';
$o->datetime = Carbon::now();
$o->tzoffset = $o->datetime->utcOffset();
$o->fftn_id = $so->match($this->ao->zone)->first()->id;
$o->tftn_id = $this->ao->id;
$o->flags = Message::FLAG_LOCAL;
$o->cost = 0;
$o->tagline = 'Address Linking...';
$o->tearline = sprintf('%s (%04X)',Setup::PRODUCT_NAME,Setup::PRODUCT_ID);
// Message
$msg = new Page;
$msg->addLogo(new ANSI(base_path('public/logo/netmail.bin')));
$header = new Thick;
$header->addText(ANSI::ansi_code([1,37]).'Clearing Houz');
$msg->addHeader($header,'FTN Mailer and Tosser',TRUE,0xc4);
$lbc = new Thin;
$lbc->addText('#link');
$msg->addLeftBoxContent($lbc);
$msg->addText(sprintf(
"Hi %s,\r\r".
"This message is to link your address [%s] to your user ID in the Clearing Houz web site.\r\r".
"If you didnt start this process, then you can safely ignore this netmail. But if you wanted to link this address, please head over to [%s] and paste in the following:\r\r%s\r",
$this->ao->system->sysop,
$this->ao->ftn3d,
url('/link'),
$this->ao->set_activation($this->uo)
));
$o->msg = $msg->render();
$o->save();
return $o;
}
}

View File

@ -34,7 +34,7 @@ class NetmailChannel
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return \Psr\Http\Message\ResponseInterface|null
* @return \Psr\Http\Message\ResponseInterface|void
*/
public function send($notifiable,Notification $notification)
{
@ -42,9 +42,8 @@ class NetmailChannel
return;
$o = $notification->toNetmail($notifiable);
Log::info(sprintf('%s:Test Netmail created [%s]',self::LOGKEY,$o->id));
Job::dispatch($ao);
Log::info(sprintf('%s:Dispatched job to pool address [%s]',self::LOGKEY,$ao->ftn));
Log::info(sprintf('%s:Sent netmail [%s] via [%s]',self::LOGKEY,$o->msgid,$ao->ftn));
}
}

View File

@ -64,7 +64,7 @@ class NetmailTest extends Notification //implements ShouldQueue
$o->datetime = Carbon::now();
$o->tzoffset = $o->datetime->utcOffset();
$o->fftn_id = $so->match($this->ao->zone)->first();
$o->fftn_id = $so->match($this->ao->zone)->first()->id;
$o->tftn_id = $this->ao->id;
$o->flags = Message::FLAG_LOCAL;
$o->cost = 0;

View File

@ -26,6 +26,18 @@ class SystemPolicy
return ($user->isAdmin() || (! $system->exists));
}
/**
* Can the user register this system
*
* @param User $user
* @param System $system
* @return bool
*/
public function register(User $user,System $system): bool
{
return ! $system->users->count() || $system->users->has($user);
}
/**
* Determine whether the user can update the model.
*

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddValidated extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('addresses', function (Blueprint $table) {
$table->boolean('validated')->default(FALSE);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('addresses', function (Blueprint $table) {
$table->dropColumn('validated');
});
}
}

View File

@ -13,7 +13,10 @@
<div class="row">
<div class="col-12">
<p>This system is aware of the following systems @can('create',(new \App\Models\System))(you can <a href="{{ url('ftn/system/addedit') }}">add</a> more)@endcan:</p>
<p>This system is aware of the following systems
@can('create',(new \App\Models\System))(you can <a href="{{ url('user/system/register') }}">register</a> more):@endcan
@can('admin',(new \App\Models\System))(you can <a href="{{ url('ftn/system/addedit') }}">add</a> more):@endcan
</p>
@if (\App\Models\System::active()->count() == 0)
@can('create',(new \App\Models\System))
@ -39,7 +42,7 @@
<tbody>
@foreach (\App\Models\System::active()->with(['addresses.zone.domain'])->get() as $oo)
<tr>
<td><a href="{{ url('ftn/system/addedit',[$oo->id]) }}">{{ $oo->id }}</a></td>
<td><a href="{{ url('ftn/system/addedit',[$oo->id]) }}" @cannot('update',$oo)class="disabled" @endcannot>{{ $oo->id }}</a></td>
<td>{{ $oo->name }} @if(! $oo->active)<span class="float-end"><small>[i]</small></span>@endif</td>
<td>{{ $oo->sysop }}</td>
<td>{{ $oo->location }}</td>
@ -72,7 +75,7 @@
<script type="text/javascript">
$(document).ready(function() {
$('table tr').click(function() {
var href = $(this).find('a').attr('href');
var href = $(this).find('a:not(.disabled)').attr('href');
if (href)
window.location = href;

View File

@ -0,0 +1,178 @@
@extends('layouts.app')
@section('htmlheader_title')
Link
@endsection
@section('content')
<form class="row g-0 needs-validation" method="post" novalidate>
@csrf
<input type="hidden" id="address_id" name="address_id">
<div class="row">
<div class="col-12">
<div class="greyframe titledbox shadow0xb0">
<h2 class="cap">Enter your Link code</h2>
<!-- ADDRESS -->
<div class="row" style="z-index: 2;">
<div class="col-4">
<label for="address" class="form-label">Address</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-globe"></i></span>
<input type="text" class="form-control @error('address_id') is-invalid @enderror" id="address" placeholder="Address" name="address" value="{{ old('address') }}" required autofocus autocomplete="off">
<span id="search-icon" style="z-index: 4;width: 0;"><i style="border-radius: 50%;" class="spinner-border spinner-border-sm text-dark d-none"></i></span>
<div id="address_search_results"></div>
<span class="invalid-feedback" role="alert">
@error('address_id')
{{ $message }}
@else
A address is required.
@enderror
</span>
</div>
</div>
</div>
<!-- CODE -->
<div class="row">
<div class="col-4">
<label for="code" class="form-label">Code</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-fingerprint"></i></span>
<input type="text" class="form-control @error('code') is-invalid @enderror" id="code" placeholder="Code" name="code" value="{{ old('code') }}" required>
<span class="invalid-feedback" role="alert">
@error('code')
{{ $message }}
@else
A code is required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<a href="{{ url('home') }}" class="btn btn-danger">Cancel</a>
<button type="submit" name="submit" class="btn btn-success float-end">Validate</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
@endsection
@section('page-css')
<style>
input#address + span {
left: -1.5em;
top: 0.5em;
position:relative
}
div#address_search_results ul {
color:#eeeeee;
background-color:#292929;
font-size: .85rem;
padding: 0 5px 0 5px;
z-index: 99;
top: -0.5em;
left: 31em !important;
}
div#address_search_results ul li.dropdown-header {
display: block;
color: #fff !important;
}
div#address_search_results ul li,
div#address_search_results ul li a {
display: block;
color: #aaa !important;
margin: 0 !important;
border: 0 !important;
width: inherit;
text-indent: 0 !important;
padding-left: 0 !important;
}
div#address_search_results ul li:hover {
padding-left: 0;
text-indent: 0;
}
div#address_search_results ul li:before {
content:""!important
}
</style>
@append
@section('page-scripts')
<script type="text/javascript">
var address_id;
var addresssearch = _.debounce(function(url,query,process,icon){
icon = $('#search-icon').find('i');
$.ajax({
url : url,
type : 'GET',
data : 'term=' + query,
dataType : 'JSON',
async : true,
cache : false,
beforeSend : function() {
if (c++ == 0) {
icon.removeClass('d-none');
}
},
success : function(data) {
// if json is null, means no match, won't do again.
if(data==null || (data.length===0)) return;
process(data);
},
complete : function() {
if (--c == 0) {
icon.addClass('d-none');
}
}
})
}, 500);
$(document).ready(function() {
$('input[id=address]').typeahead({
autoSelect: false,
scrollHeight: 10,
theme: 'bootstrap5',
delay: 500,
minLength: 3,
items: {{ $search_limit ?? 5 }},
fitToElement: false,
selectOnBlur: false,
appendTo: "#address_search_results",
source: function (query,process) {
addresssearch('{{ url('api/addresses/orphan') }}',query,process);
},
matcher: function () { return true; },
// Disable sorting and just return the items (items should by the ajax method)
sorter: function(items) {
return items;
},
updater: function (item) {
console.log(item);
$('#address_id').val(item.id);
console.log($('#address_id'));
return item.name;
},
})
.on('keyup keypress', function(event) {
var key = event.keyCode || event.which;
if (key === 13) {
event.preventDefault();
return false;
}
});
});
</script>
@endsection

View File

@ -119,7 +119,7 @@
$.ajax({
url : '{{ url('user/system/register') }}',
type : 'POST',
data : { system_id: system_id,name: $('#name').val(),action: 'create',old: {!! json_encode(old()) !!} },
data : { system_id: system_id,name: $('#name').val(),action: 'register',old: {!! json_encode(old()) !!} },
dataType : 'json',
async : true,
cache : false,

View File

@ -0,0 +1,13 @@
<!-- $o = System::class -->
<form class="row g-0 needs-validation" method="post" autocomplete="off" novalidate>
@csrf
@if($validate->count())
<p>OK, here's what we are going to do. I'm going to send you a routed netmail with a code - please follow the instructions
in that netmail.</p>
<p>Once the code is validated, this system will be assigned to you.</p>
@else
<p>I cant validate that <strong class="highlight">{{ $o->name }}</strong> is your system, we share now common zones.</p>
<p>You might want to talk to an admin.</p>
@endif
</form>

View File

@ -21,6 +21,7 @@ Route::middleware(['auth:api'])->group(function () {
Route::get('hosts/{o}/{region}',[DomainController::class,'api_hosts'])
->where('o','[0-9]+');
Route::get('systems/orphan',[SystemController::class,'api_orphan']);
Route::get('addresses/orphan',[SystemController::class,'api_orphan_address']);
Route::get('hubs/{o}/{host}',[DomainController::class,'api_hubs'])
->where('o','[0-9]+');
Route::post('default/{o}',[ZoneController::class,'api_default'])

View File

@ -40,8 +40,9 @@ Route::get('admin/switch/stop',[UserSwitchController::class,'user_switch_stop'])
Route::get('/',[HomeController::class,'home']);
Route::view('about','about');
Route::middleware(['verified','activeuser'])->group(function () {
Route::middleware(['auth','verified','activeuser'])->group(function () {
Route::get('dashboard',[UserController::class,'dashboard']);
Route::match(['get','post'],'link',[UserController::class,'link']);
Route::get('ftn/domain',[DomainController::class,'home']);
Route::match(['get','post'],'ftn/domain/addedit/{o?}',[DomainController::class,'add_edit'])
@ -82,8 +83,7 @@ Route::middleware(['verified','activeuser'])->group(function () {
Route::match(['get','post'],'ftn/zone/addedit/{o?}',[ZoneController::class,'add_edit'])
->where('o','[0-9]+');
Route::get('user/system/register',[UserController::class,'register']);
Route::post('user/system/register',[SystemController::class,'system_register']);
Route::match(['get','post'],'user/system/register',[SystemController::class,'system_register']);
});
Route::get('network/{o}',[HomeController::class,'network']);