diff --git a/app/Http/Controllers/RsPrefixesController.php b/app/Http/Controllers/RsPrefixesController.php new file mode 100644 index 000000000..8c066caf6 --- /dev/null +++ b/app/Http/Controllers/RsPrefixesController.php @@ -0,0 +1,147 @@ + + * @author Yann Robin + * @category Controller + * @copyright Copyright (C) 2009-2017 Internet Neutral Exchange Association Company Limited By Guarantee + * @license http://www.gnu.org/licenses/gpl-2.0.html GNU GPL V2.0 + */ +class RsPrefixesController extends Controller { + + /** + * Display all the RsPrefixes + * + * @return View + */ + public function list( ): View { + + return view( 'rs-prefixes/list' )->with([ + 'types' => RSPrefixEntity::$SUMMARY_TYPES_FNS, + 'rsRouteTypes' => array_keys( RSPrefixEntity::$ROUTES_TYPES_FNS ), + 'cust_prefixes' => D2EM::getRepository( RSPrefixEntity::class )->aggregateRouteSummaries() + ]); + } + + /** + * Display all the RsPrefixes for a Customer in Restricted version for the user type CustUser + * + * @param int|null $protocol protocol selected + * @return View + */ + public function viewRestricted( $protocol = null ) { + if( Auth::getUser()->getPrivs() != UserEntity::AUTH_CUSTADMIN || Auth::getUser()->getPrivs() != UserEntity::AUTH_SUPERUSER ) { + abort( 403 ); + } + + return $this->view( Auth::getUser()->getCustomer()->getId() , null, $protocol); + } + + /** + * Display all the RsPrefixes for a Customer filtered by protocol or for all protocol + * + * @param int $cid customer ID + * @param int|null $protocol protocol selected + * @return View + */ + public function viewFiltered( $cid, $protocol = null ) { + return $this->view( $cid, null, $protocol); + } + + /** + * Display all the RsPrefixes for a Customer + * + * @param int $cid customer ID + * @param string $type type of Rs prefix (adv_nacc|adv_acc|nadv_acc) + * @param int|null $protocol protocol selected + * @return View + */ + public function view( $cid, $type , $protocol = null ) : View { + /** @var CustomerEntity $c */ + if( !( $c = D2EM::getRepository( CustomerEntity::class )->find( $cid ) ) ) { + abort(404); + } + + if( $type ){ + if( !in_array( $type, array_keys( RSPrefixEntity::$SUMMARY_TYPES_FNS ) ) ) { + abort(404); + } + } else{ + $type = false; + } + + + if( !in_array( $protocol, [ 4, 6 ] ) ){ + $protocol = false; + } + + // does the customer have VLAN interfaces that filtering is disabled on? + $totalVlanInts = 0; + $filteredVlanInts = 0; + + foreach( $c->getVirtualInterfaces() as $vi ) { + /** @var VirtualInterfaceEntity $vi */ + foreach( $vi->getVlanInterfaces() as $vli ) { + /** @var VlanInterfaceEntity $vli */ + if( $vli->getVlan()->getPrivate() ){ + continue; + } + + if( $vli->getIrrdbfilter() ){ + $filteredVlanInts++; + } + $totalVlanInts++; + } + } + + return view( 'rs-prefixes/view' )->with([ + 'totalVl' => $totalVlanInts, + 'filteredVl' => $filteredVlanInts , + 'protocol' => $protocol, + 'type' => $type, + 'rsRouteTypes' => array_keys( RSPrefixEntity::$ROUTES_TYPES_FNS ), + 'c' => $c, + 'aggRoutes' => D2EM::getRepository( RSPrefixEntity::class )->aggregateRoutes( $c->getId(), $protocol ? $protocol : null ) + ]); + + + } + +} diff --git a/app/Utils/Foil/Extensions/IXP.php b/app/Utils/Foil/Extensions/IXP.php index 7b569f491..ac72c0800 100644 --- a/app/Utils/Foil/Extensions/IXP.php +++ b/app/Utils/Foil/Extensions/IXP.php @@ -59,6 +59,7 @@ public function provideFunctions() { 'scaleBits' => [ $this, 'scaleBits' ], 'scaleBytes' => [ $this, 'scaleBytes' ], 'softwrap' => [ $this, 'softwrap' ], + 'asNumber' => [ $this, 'asNumber' ], ]; } @@ -281,5 +282,17 @@ public function as112UiActive(): bool return boolval( config( 'ixp.as112.ui_active', false ) ); } + /** + * Replaces an AS Number with some JS magic to invoke a bootbox. + * + * @param string $asn The AS number + * + * @return html + */ + public function asNumber( $asn ) + { + return '' . $asn . ''; + } + } diff --git a/application/views/customer/overview.phtml b/application/views/customer/overview.phtml index 629628eda..67c5f9620 100644 --- a/application/views/customer/overview.phtml +++ b/application/views/customer/overview.phtml @@ -169,7 +169,7 @@ {if not isset( $options['frontend']['disabled']['rs-prefixes'] ) or not $options['frontend']['disabled']['rs-prefixes'] } {if $cust->isRouteServerClient()}
  • - getId()}"> + RS Prefixes {if $rsRoutes.adv_nacc.total gt 0} {$rsRoutes.adv_nacc.total} diff --git a/application/views/dashboard/index.phtml b/application/views/dashboard/index.phtml index 75e8848d2..f20b22756 100644 --- a/application/views/dashboard/index.phtml +++ b/application/views/dashboard/index.phtml @@ -36,7 +36,7 @@ {if not isset( $options['frontend']['disabled']['rs-prefixes'] ) or not $options['frontend']['disabled']['rs-prefixes'] } {if $user->getCustomer()->isRouteServerClient()}
  • - + Prefixes {if $rsRoutes.adv_nacc.total gt 0} {$rsRoutes.adv_nacc.total} diff --git a/bower.json b/bower.json index ce56f81d1..3810c3b22 100644 --- a/bower.json +++ b/bower.json @@ -30,6 +30,7 @@ "clipboard": "^1.6.1", "ip-address": "^5.0", "moment": "^2.18.1", - "select2": "^4.0.3" + "select2": "^4.0.3", + "bootbox.js": "^4.4.0" } } diff --git a/database/Entities/Customer.php b/database/Entities/Customer.php index ab3b808fe..8e4d91a3c 100644 --- a/database/Entities/Customer.php +++ b/database/Entities/Customer.php @@ -901,7 +901,7 @@ public function removeVirtualInterface(\Entities\VirtualInterface $virtualInterf /** * Get VirtualInterfaces * - * @return Doctrine\Common\Collections\Collection + * @return Doctrine\Common\Collection */ public function getVirtualInterfaces() { diff --git a/database/Entities/VirtualInterface.php b/database/Entities/VirtualInterface.php index e39174a95..16e450de5 100644 --- a/database/Entities/VirtualInterface.php +++ b/database/Entities/VirtualInterface.php @@ -357,7 +357,7 @@ public function removeVlanInterface(\Entities\VlanInterface $vlanInterfaces) /** * Get VlanInterfaces * - * @return \Doctrine\Common\Collections\Collection|VlanInterface[] + * @return \Doctrine\Common\Collection */ public function getVlanInterfaces() { diff --git a/database/Repositories/RSPrefix.php b/database/Repositories/RSPrefix.php index af059f5d4..ba892649d 100644 --- a/database/Repositories/RSPrefix.php +++ b/database/Repositories/RSPrefix.php @@ -267,8 +267,10 @@ public function aggregateRoutes( $cust, $protocol = null ) { $aggRoutes = []; - foreach( \Entities\RSPrefix::$ROUTES_TYPES_FNS as $type => $fn ) + foreach( \Entities\RSPrefix::$ROUTES_TYPES_FNS as $type => $fn ){ $aggRoutes[ $type ] = $this->$fn( $protocol, $cust ); + } + return $aggRoutes; } @@ -353,6 +355,7 @@ public function getRoutes( $irrdb, $rsOriginIsNull, $protocol = null, $cust = nu if( $cust !== null ) $query->setParameter( 3, $cust ); + return $query->getArrayResult(); } diff --git a/public/css/ixp-manager.css b/public/css/ixp-manager.css index 51ffe8961..a1c00c1c7 100644 --- a/public/css/ixp-manager.css +++ b/public/css/ixp-manager.css @@ -592,3 +592,22 @@ td a:hover{ .help-block { display: none; } + +.badge-primary{ + color: #fff; + background-color: #007bff; +} + +.badge-success{ + color: #fff; + background-color: #5cb85c; +} + +.badge-danger{ + color: #fff; + background-color: #d9534f; +} +.badge-warning{ + color: #fff; + background-color: #f0ad4e; +} \ No newline at end of file diff --git a/public/js/bootbox.min.js b/public/js/bootbox.min.js deleted file mode 100644 index 0dc0cbd5f..000000000 --- a/public/js/bootbox.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * bootbox.js v4.4.0 - * - * http://bootboxjs.com/license.txt - */ -!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
    ",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
    ",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
    "),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.css("margin-top","-10px").prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p}); \ No newline at end of file diff --git a/public/js/ixp-manager.js b/public/js/ixp-manager.js index fc41ec2ca..095c2b4f7 100644 --- a/public/js/ixp-manager.js +++ b/public/js/ixp-manager.js @@ -112,3 +112,33 @@ function ixpRandomString( length = 12 ) { return result; } + + +/** + * Replaces an AS Number with some JS magic to invoke a BootBox. + * + * @param string asNumber The AS number + * + * @return html + */ +function asnumber( asNumber ) { + + event.preventDefault(); + + let html = ``; + + bootbox.dialog({ + message: html, + size: "large", + title: "Informations", + buttons: { + cancel: { + label: 'Close', + callback: function () { + $('.bootbox.modal').modal('hide'); + return false; + } + } + } + }); +} \ No newline at end of file diff --git a/resources/views/layouts/ixpv4.foil.php b/resources/views/layouts/ixpv4.foil.php index 32480007c..011e8efa7 100644 --- a/resources/views/layouts/ixpv4.foil.php +++ b/resources/views/layouts/ixpv4.foil.php @@ -139,7 +139,7 @@ - + +append() ?> \ No newline at end of file diff --git a/resources/views/rs-prefixes/view-route.foil.php b/resources/views/rs-prefixes/view-route.foil.php new file mode 100644 index 000000000..f50fc0529 --- /dev/null +++ b/resources/views/rs-prefixes/view-route.foil.php @@ -0,0 +1,25 @@ +
    + + + + + + type == 'adv_acc' || $t->type == 'adv_nacc' ): ?> + + + + + aggRoutes[ $t->type ] as $r ): ?> + + + + + type == 'adv_acc' || $t->type == 'adv_nacc' ): ?> + + + + + +
    PrefixProtocolFirst SeenOrigin AS
    ee( $r[ 'prefix'] ) ?>IPvee( $r[ 'protocol'] ) ?> + format('Y-m-d H:i:s') ?> + asNumber( $r[ 'rsorigin' ] ) ?>
    \ No newline at end of file diff --git a/resources/views/rs-prefixes/view.foil.php b/resources/views/rs-prefixes/view.foil.php new file mode 100644 index 000000000..fee360b16 --- /dev/null +++ b/resources/views/rs-prefixes/view.foil.php @@ -0,0 +1,128 @@ +layout( 'layouts/ixpv4' ); +/** @var object $t */ +?> + +isSuperUser() ): ?> + section( 'title' ) ?> +
    ">Route Server Prefixes + append() ?> + + section( 'page-header-postamble' ) ?> +
  • + c->getId() ?>" > + ee( $t->c->getName() ) ?> + + [asNumber( $t->c->getAutsys() ) ?>] + protocol ): ?> + [IPvprotocol ?>] + +
  • + append() ?> + + section( 'page-header-preamble' ) ?> +
  • +
    + + +
    +
  • + append() ?> + + section('title') ?> + Route Server Prefix Analysis + stop() ?> + section( 'page-header-preamble' ) ?> +
  • +
    + + +
    +
  • + append() ?> + + +section( 'content' ) ?> + + totalVl != $t->filteredVl ): ?> + + + + + + +
    +
    + insert( 'rs-prefixes/view-route', [ 'type' => 'adv_nacc', 'protocol' => $t->protocol, 'aggRoutes' => $t->aggRoutes ] ); ?> +
    +
    + insert( 'rs-prefixes/view-route', [ 'type' => 'adv_acc', 'protocol' => $t->protocol, 'aggRoutes' => $t->aggRoutes ] ); ?> +
    +
    + insert( 'rs-prefixes/view-route', [ 'type' => 'nadv_acc', 'protocol' => $t->protocol, 'aggRoutes' => $t->aggRoutes ] ); ?> +
    +
    + insert( 'rs-prefixes/help', [ 'c' => $t->c, 'aggRoutes' => $t->aggRoutes ] ); ?> +
    +
    + +append() ?> + +section( 'scripts' ) ?> + +append() ?> diff --git a/routes/web-auth-superuser.php b/routes/web-auth-superuser.php index b98ee47eb..4c2c6e1c7 100644 --- a/routes/web-auth-superuser.php +++ b/routes/web-auth-superuser.php @@ -114,6 +114,11 @@ }); }); +Route::group( [ 'prefix' => 'rs-prefixes' ], function() { + Route::get( 'list', 'RsPrefixesController@list' )->name( 'rs-prefixes@list' ); + Route::get( 'view/{cid}/type/{type}/{protocol?}', 'RsPrefixesController@view' )->name( 'rs-prefixes@view' ); + Route::get( 'view/{cid}/{protocol?}', 'RsPrefixesController@viewFiltered' )->name( 'rs-prefixes@viewFiltered' ); +}); Route::get( 'admin', 'AdminController@dashboard' )->name( 'admin@dashboard' ); diff --git a/routes/web-auth.php b/routes/web-auth.php index 577884745..7d6b9b8b2 100644 --- a/routes/web-auth.php +++ b/routes/web-auth.php @@ -16,3 +16,6 @@ Route::get( 'view/{id}', 'PatchPanelPortController@view' ); }); +Route::group( [ 'prefix' => 'rs-prefixes' ], function() { + Route::get( 'view-restrict/{protocol?}', 'RsPrefixesController@viewRestricted' )->name( 'rs-prefixes@viewRestricted' ); +}); \ No newline at end of file