diff --git a/app/Http/Controllers/DomainController.php b/app/Http/Controllers/DomainController.php index 20378ab..7dd3ed6 100644 --- a/app/Http/Controllers/DomainController.php +++ b/app/Http/Controllers/DomainController.php @@ -2,12 +2,21 @@ namespace App\Http\Controllers; +use Illuminate\Support\Collection; use Illuminate\Http\Request; -use App\Models\Domain; +use App\Models\{Address,Domain,Zone}; class DomainController extends Controller { + public const NODE_ZC = 1<<1; // Zone + public const NODE_RC = 1<<2; // Region + public const NODE_NC = 1<<3; // Host + public const NODE_HC = 1<<4; // Hub + + // http://ftsc.org/docs/frl-1002.001 + public const NUMBER_MAX = 0x7fff; + public function __construct() { $this->middleware('auth'); @@ -40,6 +49,69 @@ class DomainController extends Controller ->with('o',$o); } + /** + * Get all the hosts for a zone of a particular region (or not) + * + * @param Zone $o + * @param int $region + * @return Collection + */ + public function api_hosts(Zone $o,int $region): Collection + { + $oo = Address::where('role',self::NODE_NC) + ->where('zone_id',$o->id) + ->when($region,function($query,$region) { return $query->where('region_id',$region)->where('node_id','<>',0); }) + ->when((! $region),function($query) use ($region) { return $query->whereNull('region_id'); }) + ->where('point_id',0) + ->with(['system']) + ->get(); + + return $oo->map(function($item) { + return ['id'=>$item->host_id,'value'=>sprintf('%s %s',$item->ftn,$item->system->name)]; + }); + } + + /** + * Find all the hubs for a host + * + * @param Zone $o + * @param int $host + * @return Collection + */ + public function api_hubs(Zone $o,int $host): Collection + { + $oo = Address::where('role',self::NODE_HC) + ->where('zone_id',$o->id) + ->when($host,function($query,$host) { return $query->where('host_id',$host)->where('node_id','<>',0); }) + ->with(['system']) + ->get(); + + return $oo->map(function($item) { + return ['id'=>$item->host_id,'value'=>sprintf('%s %s',$item->ftn,$item->system->name)]; + }); + } + + /** + * Get all the regions for a zone + * + * @param Zone $o + * @return Collection + */ + public function api_regions(Zone $o): Collection + { + $oo = Address::where('role',self::NODE_RC) + ->where('zone_id',$o->id) + ->where('node_id',0) + ->where('point_id',0) + ->orderBy('region_id') + ->with(['system']) + ->get(); + + return $oo->map(function($item) { + return ['id'=>$item->region_id,'value'=>sprintf('%s %s',$item->ftn,$item->system->location)]; + }); + } + public function home() { return view('domain.home'); diff --git a/app/Http/Controllers/SystemController.php b/app/Http/Controllers/SystemController.php index 0c5cbbd..d3b2131 100644 --- a/app/Http/Controllers/SystemController.php +++ b/app/Http/Controllers/SystemController.php @@ -4,7 +4,8 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; -use App\Models\System; +use App\Models\{Address,System}; +use App\Rules\TwoByteInteger; class SystemController extends Controller { @@ -13,6 +14,171 @@ class SystemController extends Controller $this->middleware('auth'); } + /** + * Add an address to a system + * + * @param Request $request + * @param System $o + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function add_address(Request $request,System $o) + { + $this->authorize('admin',$o); + session()->flash('add_address',TRUE); + + $request->validate([ + 'action' => 'required|in:region,host,node', + 'zone_id' => 'required|exists:zones,id', + ]); + + switch ($request->post('action')) { + case 'region': + $request->validate([ + 'region_id_new' => [ + 'required', + new TwoByteInteger, + function ($attribute,$value,$fail) { + // Check that the region doesnt already exist + $o = Address::where(function($query) use ($value) { + return $query->where('region_id',$value) + ->whereNULL('host_id') + ->where('node_id',0) + ->where('point_id',0) + ->where('role',DomainController::NODE_RC); + }) + // Check that a host doesnt already exist + ->orWhere(function($query) use ($value) { + return $query->where('host_id',$value) + ->where('point_id',0) + ->where('role',DomainController::NODE_NC); + }); + + if ($o->count()) { + $fail('Region or host already exists'); + } + }, + ], + ]); + + $oo = new Address; + $oo->zone_id = $request->post('zone_id'); + $oo->region_id = $request->post('region_id_new'); + $oo->node_id = 0; + $oo->point_id = 0; + $oo->role = DomainController::NODE_RC; + $oo->active = TRUE; + + $o->addresses()->save($oo); + break; + + case 'host': + $request->validate([ + 'region_id' => ['nullable',new TwoByteInteger], + 'host_id_new' => [ + 'required', + new TwoByteInteger, + function ($attribute,$value,$fail) { + // Check that the region doesnt already exist + $o = Address::where(function($query) use ($value) { + return $query->where('region_id',$value) + ->whereNULL('host_id') + ->where('node_id',0) + ->where('point_id',0) + ->where('role',DomainController::NODE_RC); + }) + // Check that a host doesnt already exist + ->orWhere(function($query) use ($value) { + return $query->where('host_id',$value) + ->where('point_id',0) + ->where('role',DomainController::NODE_NC); + }); + + if ($o->count()) { + $fail('Region or host already exists'); + } + }, + ], + 'node_id_new' => [ + 'required', + new TwoByteInteger, + function ($attribute,$value,$fail) use ($request) { + // Check that the region doesnt already exist + $o = Address::where(function($query) use ($request,$value) { + return $query + ->where('host_id',$request->post('host_id_new')) + ->where('node_id',$value) + ->where('point_id',0) + ->where('role',DomainController::NODE_RC); + }); + + if ($o->count()) { + $fail('Host already exists'); + } + }, + ] + ]); + + $oo = new Address; + $oo->zone_id = $request->post('zone_id'); + $oo->region_id = ($x=$request->post('region_id')) == 'no' ? NULL : $x; + $oo->host_id = $request->post('host_id_new'); + $oo->node_id = $request->post('node_id_new'); + $oo->point_id = 0; + $oo->role = DomainController::NODE_NC; + $oo->active = TRUE; + + $o->addresses()->save($oo); + break; + + case 'node': + $request->validate([ + 'region_id' => ['nullable',new TwoByteInteger], + 'host_id' => ['nullable',new TwoByteInteger], + 'node_id' => [ + 'required', + new TwoByteInteger, + function ($attribute,$value,$fail) use ($request) { + // Check that the region doesnt already exist + $o = Address::where(function($query) use ($request,$value) { + return $query + ->where('host_id',$request->post('host_id_new')) + ->where('node_id',$value) + ->where('point_id',0) + ->where('role',DomainController::NODE_RC); + }); + + if ($o->count()) { + $fail('Host already exists'); + } + }, + ], + 'point_id' => ['required',function($attribute,$value,$fail) { + if (! is_numeric($value) || $value > DomainController::NUMBER_MAX) + $fail(sprintf('Point numbers must be between 0 and %d',DomainController::NUMBER_MAX)); + }], + 'hub' => 'required|boolean', + ]); + + $oo = new Address; + $oo->zone_id = $request->post('zone_id'); + $oo->region_id = ($x=$request->post('region_id')) == 'no' ? NULL : $x; + $oo->host_id = $request->post('host_id'); + $oo->node_id = $request->post('node_id'); + $oo->point_id = $request->post('point_id'); + $oo->role = (! $oo->point_id) && $request->post('hub') ? DomainController::NODE_HC : NULL; + $oo->active = TRUE; + + $o->addresses()->save($oo); + break; + + default: + return redirect()->back()->withErrors(['action'=>'Unknown action: '.$request->post('action')]); + } + + return redirect()->to(sprintf('ftn/system/addedit/%d',$o->id)); + } + /** * Add or edit a node */ diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 037f28a..1b44804 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -35,6 +35,7 @@ class Kernel extends HttpKernel \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, ], 'api' => [ diff --git a/app/Models/Address.php b/app/Models/Address.php new file mode 100644 index 0000000..7aec4ad --- /dev/null +++ b/app/Models/Address.php @@ -0,0 +1,52 @@ +belongsTo(System::class); + } + + public function zone() + { + return $this->belongsTo(Zone::class); + } + + /* ATTRIBUTES */ + + /** + * Render the node name in full 5D + * + * @return string + */ + public function getFTNAttribute() + { + return sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->host_id ?: $this->region_id,$this->node_id,$this->point_id,$this->zone->domain->name); + } + + public function getRoleAttribute($value) + { + switch ($value) { + case DomainController::NODE_ZC; + return 'Zone'; + case DomainController::NODE_RC; + return 'Region'; + case DomainController::NODE_NC; + return 'Host'; + case DomainController::NODE_HC; + return 'Hub'; + case NULL: + return 'Node'; + default: + return '?'; + } + } +} diff --git a/app/Models/System.php b/app/Models/System.php index a41faa1..7a3f492 100644 --- a/app/Models/System.php +++ b/app/Models/System.php @@ -15,5 +15,14 @@ class System extends Model /* RELATIONS */ + public function addresses() + { + return $this->hasMany(Address::class) + ->orderBy('region_id') + ->orderBy('host_id') + ->orderBy('node_id') + ->orderBy('point_id'); + } + /* CASTS */ } \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 00cfcd5..b88d6c8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,40 +6,68 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Passport\HasApiTokens; +/** + * Class User + * + * User Roles: + * + Site Admin + * + ZC - For Domain/Zone + * + RC - For sub portion of a Domain/Zone (aka Region) + * + Host Admin - For sub portion of a Region + * + Hub Admin - For a sub portion of a Hosts system + * + Sysop - Individual system + * + Guest + * + * @package App\Models + */ class User extends Authenticatable implements MustVerifyEmail { - use HasFactory, Notifiable; + use HasFactory,Notifiable,HasApiTokens; - /** - * The attributes that are mass assignable. - * - * @var array - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; - /** - * The attributes that should be hidden for arrays. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; - /** - * The attributes that should be cast to native types. - * - * @var array - */ - protected $casts = [ - 'email_verified_at' => 'datetime', - ]; + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + ]; - protected $dates = ['last_on']; + protected $dates = ['last_on']; + + /* GENERAL METHODS */ + + /** + * See if the user is already a member of the chosen network + * + * @param Domain $o + * @return bool + */ + public function isMember(Domain $o): bool + { + return FALSE; + } } diff --git a/app/Models/Zone.php b/app/Models/Zone.php index c7bc931..c9a23c0 100644 --- a/app/Models/Zone.php +++ b/app/Models/Zone.php @@ -12,6 +12,11 @@ class Zone extends Model /* RELATIONS */ + public function addresses() + { + return $this->hasMany(Address::class); + } + public function domain() { return $this->belongsTo(Domain::class); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4cda910..417fc39 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\ServiceProvider; +use Laravel\Passport\Passport; class AppServiceProvider extends ServiceProvider { @@ -14,7 +15,7 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - // + Passport::ignoreMigrations(); } /** diff --git a/app/Rules/TwoByteInteger.php b/app/Rules/TwoByteInteger.php new file mode 100644 index 0000000..edcc93f --- /dev/null +++ b/app/Rules/TwoByteInteger.php @@ -0,0 +1,33 @@ + 0) && ($value < DomainController::NUMBER_MAX)) || ($value === 'no'); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return sprintf('The number must be between 1 and %d.',DomainController::NUMBER_MAX); + } +} diff --git a/composer.json b/composer.json index 8fe8349..b3ce3be 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "laravel/framework": "^8.0", + "laravel/passport": "^10.1", "laravel/ui": "^3.2", "repat/laravel-job-models": "^0.5.1" }, diff --git a/database/migrations/2019_04_16_105254_create_nodes.php b/database/migrations/2019_04_16_105254_create_nodes.php index 97055eb..dff0c8b 100644 --- a/database/migrations/2019_04_16_105254_create_nodes.php +++ b/database/migrations/2019_04_16_105254_create_nodes.php @@ -74,7 +74,6 @@ class CreateNodes extends Migration $table->integer('software_id')->nullable(); $table->foreign('software_id')->references('id')->on('software'); - // $table->unique(['zone_id','host_id','id']); // $table->index(['zone_id','host_id']); // $table->index(['zone_id','id']); diff --git a/database/migrations/2021_06_19_045817_create_addresses.php b/database/migrations/2021_06_19_045817_create_addresses.php new file mode 100644 index 0000000..5f7bf7f --- /dev/null +++ b/database/migrations/2021_06_19_045817_create_addresses.php @@ -0,0 +1,49 @@ +id(); + $table->timestamps(); + $table->boolean('active'); + + $table->integer('zone_id'); + $table->foreign('zone_id')->references('id')->on('zones'); + + $table->integer('region_id')->nullable(); + $table->integer('host_id')->nullable(); + $table->integer('node_id'); + $table->integer('point_id'); + $table->integer('status')->nullable(); // @note Used to record Down/Private/Pending, etc + + $table->integer('role')->nullable(); + + $table->integer('system_id'); + $table->foreign('system_id')->references('id')->on('systems'); + + $table->unique(['zone_id','region_id','host_id','node_id']); + $table->unique(['zone_id','host_id','node_id','point_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('addresses'); + } +} diff --git a/database/migrations/2021_06_20_124638_unique_relations.php b/database/migrations/2021_06_20_124638_unique_relations.php new file mode 100644 index 0000000..2fc1565 --- /dev/null +++ b/database/migrations/2021_06_20_124638_unique_relations.php @@ -0,0 +1,50 @@ +unique(['node_id','setup_id']); + }); + Schema::table('domain_user', function (Blueprint $table) { + $table->unique(['domain_id','user_id']); + }); + Schema::table('node_system', function (Blueprint $table) { + $table->unique(['node_id','system_id']); + }); + Schema::table('system_user', function (Blueprint $table) { + $table->unique(['system_id','user_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('node_setup', function (Blueprint $table) { + $table->dropUnique(['node_id','setup_id']); + }); + Schema::table('domain_user', function (Blueprint $table) { + $table->dropUnique(['domain_id','user_id']); + }); + Schema::table('node_system', function (Blueprint $table) { + $table->dropUnique(['node_id','system_id']); + }); + Schema::table('system_user', function (Blueprint $table) { + $table->dropUnique(['system_id','user_id']); + }); + } +} diff --git a/public/oldschool/css/main.css b/public/oldschool/css/main.css index d84709e..6ad8ed6 100644 --- a/public/oldschool/css/main.css +++ b/public/oldschool/css/main.css @@ -590,19 +590,6 @@ tbody { border-bottom:1px solid #666 } -.push-right { - float:right; -} -.text-center { - text-align:center; -} -.text-left { - text-align:left; -} -.text-right { - text-align:right; -} - .titledbox { margin:1.5em auto 2.5em auto } diff --git a/resources/views/domain/home.blade.php b/resources/views/domain/home.blade.php index bbb0062..e0292c8 100644 --- a/resources/views/domain/home.blade.php +++ b/resources/views/domain/home.blade.php @@ -7,7 +7,7 @@
In FTN network addresses, a domain is the 5th dimension and used when a system supports 5D addressing, ie: zone:hub/host.point@domain.
+In FTN network addresses, a domain is the 5th dimension and used when a system supports 5D addressing, ie: zone:net/node.point@domain.
Domains are used with zones to uniquely identify a FTN network.
Some legacy Fidonet software is not 5D aware and may behave unexpectedly when a domain is used