From 4f11deeaf9cd6084c453985f824d070a0e7855e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Thu, 23 Sep 2021 17:25:09 +0200 Subject: [PATCH] Add some basic subscriptions statistics Move the statistics to a specific Library Show some subscriptions charts in the admin panel Inject the browser user agent to the database if available Split statistics in different view (day/week/month) Install cron scripts Update the dependencies --- Makefile | 1 + cron/flexiapi.debian | 6 + cron/flexiapi.redhat | 6 + flexiapi/app/Helpers/Utils.php | 6 + .../Account/RegisterController.php | 4 +- .../Admin/StatisticsController.php | 53 +++++ .../Api/Admin/AccountController.php | 2 +- .../Controllers/Api/StatisticController.php | 43 ++++ flexiapi/app/Libraries/StatisticsCruncher.php | 197 ++++++++++++++++++ flexiapi/composer.phar | Bin 2256047 -> 2262733 bytes flexiapi/config/app.php | 2 +- ...6_132040_add_accounts_tombstones_table.php | 17 ++ ...nique_accounts_passwords_aliases_table.php | 17 ++ flexiapi/public/css/charts.css | 77 +++++++ .../account/documentation_markdown.blade.php | 10 +- .../resources/views/account/panel.blade.php | 6 + .../admin/statistics/parts/columns.blade.php | 12 ++ .../admin/statistics/parts/legend.blade.php | 6 + .../views/admin/statistics/show_day.blade.php | 37 ++++ .../admin/statistics/show_month.blade.php | 37 ++++ .../admin/statistics/show_week.blade.php | 37 ++++ .../api/documentation_markdown.blade.php | 11 + .../resources/views/layouts/base.blade.php | 1 + flexiapi/routes/api.php | 4 + flexiapi/routes/web.php | 4 + flexisip-account-manager.spec | 12 +- 26 files changed, 601 insertions(+), 7 deletions(-) create mode 100644 cron/flexiapi.debian create mode 100644 cron/flexiapi.redhat create mode 100644 flexiapi/app/Http/Controllers/Admin/StatisticsController.php create mode 100644 flexiapi/app/Http/Controllers/Api/StatisticController.php create mode 100644 flexiapi/app/Libraries/StatisticsCruncher.php create mode 100644 flexiapi/public/css/charts.css create mode 100644 flexiapi/resources/views/admin/statistics/parts/columns.blade.php create mode 100644 flexiapi/resources/views/admin/statistics/parts/legend.blade.php create mode 100644 flexiapi/resources/views/admin/statistics/show_day.blade.php create mode 100644 flexiapi/resources/views/admin/statistics/show_month.blade.php create mode 100644 flexiapi/resources/views/admin/statistics/show_week.blade.php diff --git a/Makefile b/Makefile index 50584e4..578fdcf 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ package-common: # General cp README.md $(OUTPUT_DIR)/flexisip-account-manager/ cp -R httpd/ $(OUTPUT_DIR)/flexisip-account-manager/ + cp -R cron/ $(OUTPUT_DIR)/flexisip-account-manager/ cp flexisip-account-manager.spec $(OUTPUT_DIR)/rpmbuild/SPECS/ tar cvf flexisip-account-manager.tar.gz -C $(OUTPUT_DIR) flexisip-account-manager diff --git a/cron/flexiapi.debian b/cron/flexiapi.debian new file mode 100644 index 0000000..b529a5c --- /dev/null +++ b/cron/flexiapi.debian @@ -0,0 +1,6 @@ +#!/bin/sh + +cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ +sudo -su www-data && php artisan digest:expired-nonces-clear 60 +sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply +sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply \ No newline at end of file diff --git a/cron/flexiapi.redhat b/cron/flexiapi.redhat new file mode 100644 index 0000000..6793c50 --- /dev/null +++ b/cron/flexiapi.redhat @@ -0,0 +1,6 @@ +#!/bin/sh + +cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ +scl enable rh-php73 "php artisan digest:expired-nonces-clear 60" +scl enable rh-php73 "php artisan accounts:clear-accounts-tombstones 7 --apply" +scl enable rh-php73 "php artisan accounts:clear-unconfirmed 30 --apply" \ No newline at end of file diff --git a/flexiapi/app/Helpers/Utils.php b/flexiapi/app/Helpers/Utils.php index be2a283..2f40026 100644 --- a/flexiapi/app/Helpers/Utils.php +++ b/flexiapi/app/Helpers/Utils.php @@ -58,6 +58,12 @@ class Utils return mt_rand(1000, 9999); } + public static function percent($value, $max) + { + if ($max == 0) $max = 1; + return round(($value*100)/$max, 2); + } + public static function markdownDocumentationView($view) { $environment = Environment::createCommonMarkEnvironment(); diff --git a/flexiapi/app/Http/Controllers/Account/RegisterController.php b/flexiapi/app/Http/Controllers/Account/RegisterController.php index fb23b82..2c7b820 100644 --- a/flexiapi/app/Http/Controllers/Account/RegisterController.php +++ b/flexiapi/app/Http/Controllers/Account/RegisterController.php @@ -91,7 +91,7 @@ class RegisterController extends Controller $account->domain = config('app.sip_domain'); $account->ip_address = $request->ip(); $account->creation_time = Carbon::now(); - $account->user_agent = config('app.name'); + $account->user_agent = $request->header('User-Agent') ?? config('app.name'); $account->save(); $account->confirmation_key = Str::random($this->emailCodeSize); @@ -143,7 +143,7 @@ class RegisterController extends Controller $account->domain = config('app.sip_domain'); $account->ip_address = $request->ip(); $account->creation_time = Carbon::now(); - $account->user_agent = config('app.name'); + $account->user_agent = $request->header('User-Agent') ?? config('app.name'); $account->save(); $alias = new Alias; diff --git a/flexiapi/app/Http/Controllers/Admin/StatisticsController.php b/flexiapi/app/Http/Controllers/Admin/StatisticsController.php new file mode 100644 index 0000000..d81d76d --- /dev/null +++ b/flexiapi/app/Http/Controllers/Admin/StatisticsController.php @@ -0,0 +1,53 @@ + $day, + 'max_day' => $maxDay, + ]); + } + + public function showWeek(Request $request) + { + $week = StatisticsCruncher::week(); + $maxWeek = 0; + foreach ($week as $day) { + if ($maxWeek < $day['all']) $maxWeek = $day['all']; + } + + return view('admin.statistics.show_week', [ + 'week' => $week, + 'max_week' => $maxWeek, + ]); + } + + public function showMonth(Request $request) + { + $month = StatisticsCruncher::month(); + $maxMonth = 0; + foreach ($month as $day) { + if ($maxMonth < $day['all']) $maxMonth = $day['all']; + } + + return view('admin.statistics.show_month', [ + 'month' => $month, + 'max_month' => $maxMonth, + ]); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php index a7cfec1..fe650e8 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php @@ -124,7 +124,7 @@ class AccountController extends Controller $account->domain = $request->has('domain') && config('app.everyone_is_admin') ? $request->get('domain') : config('app.sip_domain'); - $account->user_agent = config('app.name'); + $account->user_agent = $request->header('User-Agent') ?? config('app.name'); if (!$request->has('activated') || !(bool)$request->get('activated')) { $account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize); diff --git a/flexiapi/app/Http/Controllers/Api/StatisticController.php b/flexiapi/app/Http/Controllers/Api/StatisticController.php new file mode 100644 index 0000000..0398610 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/StatisticController.php @@ -0,0 +1,43 @@ +. +*/ + +namespace App\Http\Controllers\Api; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; + +use App\Libraries\StatisticsCruncher; + +class StatisticController extends Controller +{ + public function month(Request $request) + { + return StatisticsCruncher::month(); + } + + public function week(Request $request) + { + return StatisticsCruncher::week(); + } + + public function day(Request $request) + { + return StatisticsCruncher::day(); + } +} diff --git a/flexiapi/app/Libraries/StatisticsCruncher.php b/flexiapi/app/Libraries/StatisticsCruncher.php new file mode 100644 index 0000000..4fb2c31 --- /dev/null +++ b/flexiapi/app/Libraries/StatisticsCruncher.php @@ -0,0 +1,197 @@ +. +*/ + +namespace App\Libraries; + +use Illuminate\Support\Facades\DB; + +use Carbon\Carbon; +use Carbon\CarbonPeriod; +use Carbon\CarbonInterval; + +use App\Account; + + +class StatisticsCruncher +{ + public static function month() + { + $data = self::getAccountFrom(Carbon::now()->subMonth()) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliases = self::getAccountFrom(Carbon::now()->subMonth()) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataActivated = self::getAccountFrom(Carbon::now()->subMonth()) + ->where('activated', true) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliasesActivated = self::getAccountFrom(Carbon::now()->subMonth()) + ->where('activated', true) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + return self::compileStatistics( + collect(CarbonPeriod::create(Carbon::now()->subMonth(), Carbon::now()))->map->format('Y-m-d'), + $data, + $dataAliases, + $dataActivated, + $dataAliasesActivated + ); + } + + public static function week() + { + $data = self::getAccountFrom(Carbon::now()->subWeek()) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliases = self::getAccountFrom(Carbon::now()->subWeek()) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataActivated = self::getAccountFrom(Carbon::now()->subWeek()) + ->where('activated', true) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliasesActivated = self::getAccountFrom(Carbon::now()->subWeek()) + ->where('activated', true) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + return self::compileStatistics( + collect(CarbonPeriod::create(Carbon::now()->subWeek(), Carbon::now()))->map->format('Y-m-d'), + $data, + $dataAliases, + $dataActivated, + $dataAliasesActivated + ); + } + + public static function day() + { + $data = self::getAccountFrom(Carbon::now()->subDay()) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliases = self::getAccountFrom(Carbon::now()->subDay()) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataActivated = self::getAccountFrom(Carbon::now()->subDay()) + ->where('activated', true) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + $dataAliasesActivated = self::getAccountFrom(Carbon::now()->subDay()) + ->where('activated', true) + ->whereIn('id', function($query) { + $query->select('account_id') + ->from('aliases'); + }) + ->get(array( + DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"), + DB::raw('COUNT(*) as "count"') + ))->each->setAppends([])->pluck('count', 'moment'); + + return self::compileStatistics( + collect(CarbonInterval::hour()->toPeriod(Carbon::now()->subDay(), Carbon::now()))->map->format('Y-m-d H'), + $data, + $dataAliases, + $dataActivated, + $dataAliasesActivated + ); + } + + private static function getAccountFrom($date) + { + return Account::where('creation_time', '>=', $date) + ->groupBy('moment') + ->orderBy('moment', 'DESC') + ->setEagerLoads([]); + } + + private static function compileStatistics($period, $data, $dataAliases, $dataActivated, $dataAliasesActivated) + { + $stats = []; + + foreach ($period as $moment) { + $all = $data[$moment] ?? 0; + $aliases = $dataAliases[$moment] ?? 0; + $activated = $dataActivated[$moment] ?? 0; + $activatedAliases = $dataAliasesActivated[$moment] ?? 0; + + $stats[$moment] = [ + 'all' => $all, + 'phone' => $aliases, + 'email' => $all - $aliases, + 'activated_phone' => $activatedAliases, + 'activated_email' => $activated - $activatedAliases + ]; + } + + return $stats; + } +} \ No newline at end of file diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index 48916931bb810c52d1c5098402660adc89bdfd3c..81380626f4c4780fbc4e1cbf3b98925d45826424 100755 GIT binary patch delta 23358 zcmb8X33yaR)-X&b>3zF>yOU0*v)nACvu_ZxL4bs?FCl=kLz6V5B}s?w4m$=!M35zN zk)whT0Yy|6b>xl<62^CCWJI=sQ3p^^QBe_aT)>V0)UC?K_-M`+Q-viu0sr$<*ZKrk<pO|YXSBqC2J z_tiK00v^BM_snbZ`aJ=`8x(>*!Qa%7B~&!HXVrOxV68{+EDU-Y0$yJOj1WVTA_)aj z-yj72iv;%^x3?k7C1eRuU!C9(9}Jba-&geLk@)dIH-X14d@@KJXmf}$VOp_1ES$R% zLmLTfaok)dZVF2dBiQ1bVf|>~x3CgUjG$-)Zh14LNL-lpnOU4I!r*uO`QKQF8O1@RNa-*k4l zvene1NA$+sedJwQuGflX`f%rdAG24(JuF-Rx+CUo!e84)$Cokl3y-qx? zr{o+JJ$m#yx$&R!R5^p0YhF)#5P0a-XD=eDK9oPNfz4;1p_sGfQ#1lMPVF;ToM(s^ zBaM{k)VLl!4i67KB?`kF(d}}Z)mYn@#MmM5yPmH%iH$}VCBD^2w=p)PM~}Gmk-s24 zr>Hi?$aCK0=+VRR)cQflq*EMW3g_-ErK1!0*vP0>d0Hn;8u3w+gVNh?8o>2ZQ7i&a z9{O&gxFgpg&NPRM8D>3KWuc7(dcXSkMOpS{nv=MXQ6h-!5%}Y#_0z@J$PBT~Z01ge z(gp$}LkG8sADf5L!P2=QoL)~`2}~G}Y!uIP}O(>LDETK zrtJhiwC>?YaD(tjhTCnVacnwmB=BmqK7$_QaC=PNNN>>Nch?07RtQ@K0_UyzY9bjz zY>NmJTf)`c7Pfu_Ud|U>;z#7jhr74|k(4%pw_D#+ijO*?#F-HhqASA8J<7J2z<%c^ z|Bra8ROU*YA7SFEaFxlF2(16~AF=I?vG=-z)wM(X?#5ce)(|a4Bk=L6h@~CO%V?v0 zH6DUh97eGSobsu1hb(fJ><$BIG!@cDvEDwK?!^)N7|z4?g1|?Qzwj;YMG1F3ilP$u zSmS<|ERA54gOgLc&FQz;6-1%7AoxqTnwiD?jjXmk8 zYaAowuBWkU^3iQaWF`smuqd80WYMk!Hh;G_OYS-#igooy4Wcw&iW(@>h(JM@`m)>J zK_kJ*lxkmRyJB-z%BTSx?!%q>o9(EZqIqtXla5AU-nZA@M#Y~b4vvnJJ9tpR7H{4; zxV?ke63u8oAI-Gk_2_=w8fG5|{N(R(2T&0@L^7S3G2xWiMKNWZft^bNKUjRLUOXK~ zv98DTlt(*@?9JTs{f`~p2gS1P(_-07o{sIueaAQ^&|3Q14`P3fL;Na^6YX(EI$BEH zP?_NQydFKuX5H6>Bc+PZ#0j+ZSln1zh!w`lX!CL0L8A^YlE>{Y=)~;;o8_N{3{F!) z7l^=*dmX+c8WN&JQ@nxO&C><~w_f!ph~Jhu#PWD%g(k=6a^p}R62T^L`{cNHMR$cm z{8Kz5ej+}P@)4Gh#eKPeq7qnh^5O(s@kG&+VC6FHw1L1cqJRBXJ{$*o>ckfk;%N61 z31!?(rYZ<*d2`xnc`5|!M50z4naI{V-7!O$wK3C#!q($Z9?mj`o%G(u?0FQDeOY z4`jasuS{e;NP}MN+fz#i9@evz+oz+n3B1}^IUWxbArZ@Y*`Nd-`F_qg)H&=BBq!(L zzSC|5KELUhO{`7sMTzc6F5=!SpsfU|j5|J*=iHX86?HB<=VO|O!129~4-<1;x!hhR z%>-_qx9)TCRIx)`o~03|q-n+LE{nL&rQs6L{*iScaOcOxgYH6daCcC75(EM-2YRN7 zIVkO&R8->76a#mQX&wU8#?0JMe#%$$@)5eTe2d{*jMTY_*VgSwm*xbO>SS>Nj5gEaQcY?_v6-Q^&Gtmzh{in&-UhmkE z4cXC@@BP_>xa&+h33T~(t)vGr$L<`0#vUUKV$t>_QLYI*meB8ty!i)n?BcQ<9XE}w zG=V>Uc4eF_g9me%D|bGpnEM3}CD{uC-+S+6N$i=MOBtA)o6o(#1c$(n>(|C2n=F>N zl*@!KE01~6-aO`P?`Gk$2+9)WFH&% zA;utNSqOYC%JzmlugXHB_*Q{h6bkHgWlIW}DQhVx=YD0ABJdxhU)_X$uS29=EU?_G z)5BoJ>3^9QtQ&q=g z*Ri$OAg(WA=WSm}Ik!WgLlXGi<%6$wY=}@ACZ_b|xD|{i0=G<=y98}jMn5+noW0TnSSq1YUfs z`4Pm5jUCt&tR3p9YovR2h8{p91AfC-210(-^s4rjq)gN4l_suO1JOqkv;DlkTR-sjfOB` zAn=>R=X$dK1kEO|LNhib&Ha0AdqXOVUn~8-g|cdiVMm+5qvH;8?QwXbICe<*rV0z^ zVXH%6#y8s^!bQ&EYLn@b5qQQmc`5EqHeHa-6T9%AEGkD@am@IIKTlSvXDpBrM}zCzh05<#G?anL1M-;%~p zhD6KDIuCii{z(1?Oy|Vf$IWjb8y(N)L*V+!xx-P6GPn|E^$AS-R`&?$AU-ivFH@O? zm2p?5Z6T5^o)~K6s@ZlD7%1$uN+v(SUq{GWnZ+Z}+P`QQwx!Y~A2qBe_a>7U0tajw znkFv-W-Q~%>)e6BNS~V}I*yjonGraYn{opW0CD^^uz!%v$bH#|wi4LasQs6y7|#6r z!r?h|u)5*t;bdtZKvl73-)HZ%?^>LAdYn}}GTh8fWkC&r7bZ7t6Wv>_Vsw=jrli@z zxvA^~5~%&e|3G`I_}d6AJfTz=#L5w2bf}3VifLiX2>(%^kG@uS2(hEZLnE17ogB$rXX~gkI?SX|RGk@c z1OL(52;$AnR&n1bmfU@RR24B=e?6 z9zHCNZMBNiE91oDW1Mt=ug8p+F%F=NzFcAdnPIRc+g8ckm7A4p?Rt-;Dc9ZUC?UD8 zzrYq=hr{W_J!2W$uZ?Blo}!BRpJi3ddD&bwl8*dY74t~*#Gjv~X!MV{rJCxwPiBDbs1eIh7G# zJ+*<>+@8uN;hn|`A5LqM$8EyX)35dmwb*yYV6k<2yj-~eD@SL3H$}|8r>{5zRg!f^ z3?rE4i|yHp zS+nF;1+J%WZTd1%F|Duod$n0Cu4Yl@z11@)#n-FZ7%4TZu%(8rkj^uO;&?q%Y2lb> zCM{&ncFO~-!f9Rm-06`8bl+?ypkL3Wv2JcJaYvn6{JzdY`)KRO(}KIcN=AK)EO~hO5z$iLOZ={$EvUJH z&1Z6Z;hBb!GX5!)+xS<;C}jK`AHyHzV|=gju?+WR-zXWs4;n&CUg5i9Ar!6~){tYTyn_0%ME z@Tx_OYMNIkij4uiSlh&O^ok}%<8%{Cctp)-n^MJy$78YUB?0DhdPKlYT62;196p+PC193~(a!y{tk|cYUF!S?u zdrjd|CgjaanSOn5DU+ATWo-KHWo()EEo0pOx{M}Zr=X!Y&}Z3AdHOe&n#INUGv%@M zer5qay1!gzb2++tkF1ihI`PZ9iB0xa(J>$J$31YTCQOsP< z^yARwjMI(F*(p4|oXyX>3A$2#}Ke{cM59q~fD=ac6yO5K=m92A$ zN5@*llaFfTean1+iU05iC=*Ypdh}SbcH8gbw1+8)!w={wzsDb-Mm7`G_?+7DTk$wh zj14Q*;;@xeyk_7oKGIrR*}=hvmF=0Il~gh>p-8WO=x_h%#s(qL`#}~241bXA)`kb! z{+)l2&A8_(wp)Q!l-ieg_OoNoZtO;_a+Oy6Wfe!+Hb2A`X~sj$?rnXDPIW5!0Sn&W z9oE6CX*J_DYc&&r1*>VI<0PU6wikIjP;a)QS{`Q1PI#D|{*4dQ3ygy(B-1}nyNR~n zDK1~D7Jq-3X2*UTg6ZDHBV$m~NdqSiT@y=ry>Cq=Exfgc&0N2h9l>esg#&AuF#f)l zWdg=N!j9YXkFevWSjW`!;B{_F@P&2LXu+`FLkmsondQ2$zPBtN>8NpEex!AdIN(Wi zi(0hefQ@*jH?*JW4fL22kD&3|)I;K!P3>6Wa@t}wHV|Kxz~#RtVya$NSsKyQqDNBf zJm0sGE!X~yOqqSXk@1?-!sfiJh3SYBEo`@=A7w|S_E9S6Hgr3m{p`{hw(Y5OO20nJ zj_{C8blVb9?%M4a@(1dy>2bq`|ydCpz)f0hxi=S7hrb zid5G;h%y^JWN{`M^#nxCme(s4C%Q0hk4N146eUh*%-_uNG}|{bUaoFt^UHdSrVc_e z)4Zo-R4T4ug1G&$csl0Wk1^@I^B9w(qAhHH*KMJ8ZXwFL`TNjH@!bV%HlP*5L7)VR zU=l5C0cz$F@gxuFKmKsX9J$9`6r<#E>dodO!6&c9**Y-Vz$pIXakk@MK2G!Re?hG1 zji2Hph}O>&nAOUZ_OMpkJq~$qzHWL!7C?gaeru#W^5fWjr}KkialleS zS0@TjN6DgUuQ?Bk}vTNBLC*!=*dv|q3#C*4ZhG$`;&mT-l zYQ$`52EOEJ^1#t^mKZoNEL8oH+yL^P%H6OiD04K&hmWyl`{ZX<_&O^fWNKsu2%(g__Jz_SJ=44QOij}XWIx*fqf|O}y4xs>8G*2p z(!U>a)6u+ap{qov(1e@=nHymYgLDS5J5~DntM-E<`QeH&ZdzCcPe1G=xCv0!5-UVu7N9mo~0Z)P0(k zC8Xe7+<|e60zprG!D4Ttuym==Su>#lt5SvJWC1>|GsM&g{RLNs(075`Pfk~9qu*CO zgB&#h1+K;$Ag}Up1O2j4;erKlAv1&~LL|E&bFr|%TURHy;elzAvHNDX({c&Lg3Fbv zONDtu62jqHe6&V#)Q4PGz@J_=WWb2!A-bY&;}OAi&v$$4$fy~@9DE;G?QS5oGIdW) z-|T8%eLb>J>VkQEf)xfxW<8vmV+w<~>6}{1@P+6!5D2w6w>)W7!Sn?oDT)%XG=!zX zfd7Olp=A)C2zP!ns-5(l zu(0tEtu`89>l%el8rmH8ObS$fV~UVAJQsFa0Y6(pBc+<>!-h|Wd#*;=w)6_MNTHn&xC7ryh4biUKyn^6CVq6na+a z)W_oasdq0Lje4=Nu4#_90bj$_yYWo-6K3P9C}^DSOoN;pCzqf@kxJ3&NUcZc*H0FE z8FjR~-lNl@5W=AgN>_R+RsSd!xR~DALdqZ0M{4o#(l5$bY2e4oADyr-T5Ev;-^8h) zez_4hc<04X zBmAdWl?G8IsyNv5PM8tOOH?_KR&Eps4~9Iq-|t=|B(Wnq%H5bDu&SnpIv>7Xz#}Ht z4)Roc$!ptyB$cQtl<-`JDM=bNP<2ibCTod##!$P#cy9R8Cr&6Hp0oUK68(jRDmV=^XxFgs$^phLpZm z^>c`nwO+M89GAN{aMqBXaA~J%jFh!Y^>Zn>K2gQO=m2UV-zT_r2Lc8Ih@YsUXz9m9 z+RB?M7t((Irs^KJb5j*z?J9ocVK!QS7+a#ULZ9zcW~hBx$-||dDkDrAtg=b-zf=9F zfM*|4Mo7)ytBMpbbe!4(+iqAm>D?bx8CH!Ob>tv-&<*eD)pqz^qYi>ktqzB}AGI2I zM62Eke^ROKQn^mOnTzN;G?V;XJWO9lshyxnkBx>Tr}{Rm>Z`(o-%}F?8AFh*t5@QU zFg98pCpSpTqt%xc6ld8`RXmuBqEujXM4Ms9{1^_TSoPlzDi&!e6gd^w(j>GA49 z7~J1th2^U)VbbwP6vvZc%z1*g)rk+6J*(gthN)v-j^xZt0Hn(ub{+%+|x8a%~Y zZfhbTDno695oKxvWfXVK0R5(@6JdRZIxi}TYC;?yeZ79fofXJ*$9aPAbarS8a5-v~ zbTvbrqkt0`xB+=t>VK0`w3M8!Hits*JoR)*%u^3D!vkgNV=%f*tu2+OBh%|j9v83c zXZO*t(eDX1`5S~Jm|3nK0?(GKv*djk+I7W)iyA$FyLO?v$?r$oKW6r9G+fx$MRBsG z@z@I`C~c5hfV08aIHE!|Qo|ti-;~g6xH=zpe-sxkJvLk&uaI6Hq5ez_zmCAUPFf_G zVB09QQ93$KovKLCH8#zv^HvK<3y9B-GTFT%4~NiBQ0FVj0t!;;1oeykpmbT75jQRt z+IEGg;QLML-raTzc}0a=AAmm({S4k{<%`N@t|8lm;k(3mYhX>71{h+Z#^dRl!S6UPBLyb_kbY!8!8?Nr|^ zgWLD3dFjj*b+!TZhPo$J!_zIA2>4tZ#lcWtlpQMja_P|axj6w^PMK1rOW&#QRjV_E zq=2Vxwp8%5x-2XNwfK&ojdAdDsHRZ64CTiYXsQdsz;(JP>B-;KX=|bFf;tQmdzo!q zi7v@KD?r}Y;Ibsm4#4(EQ=vd>2YCZQylSNlnWf&qz*zxbT~p9g36~$%I75W~5ciWh zJR~I*4o8~okoXfSs{5bSYd|qbZ!Tro$gWf8dQ(zIQgeG%nd#jn;wZfuFXc9BV(S|jRi>O*#z_(0GdYzma)qipmFhrO=p zlK=@b>^!tw(3sHbX$0`sTX^Zj1a|nN%Hvp=-T%a62{~2ZqTJCztjnbxXT)QYNz8} z?XDv+fE$0z^GCq}Dnm^yO#CRy2%5{9sqpt5nsiuyS#!VbkA@YWz7z$of1=Tq{UI?S zfIl6QqYD>?r!|7BV6jUeK0kg0?g|Dnq^X~1;uVqdPlw3jbW$W;y>S1hn!d2gYKfHx zl+r)ZY*N6*gE|-3pW!Xie?HYbpit;wMTRa4xTiE`s87~tAn}SO)sBZN=ug2=fW#rL zY~mJAo$5-3_=y^43~QvRjLybXbZD39q~I0JreSdMrr8F~{dEbje5Ao7*|N3oB|u1( zwh(51V|KvRIEw??uEnXOV3l^A1*%@rB}s`j+V?`?!_u&LDY{l`4T0kOw6ml=_i3*x zVbptegS53#8|h5O{c@4O7N^_cH@XACQJ4h4C#e;FvY{@Qbn+2xgIa18wa=%*tV)a> zfBcI!6V|zm8ma2Ac8U_jlUj>Z@U|9ex@oFe-iAS5KdQJ(E}b1-zNLwVeK%1x#-`|A zkC)ybrn?;r|1$&Gy;`f=VTEtsLb<)6QDPXjlUGCIOIj^Fyi|9u%rV@5TB(od!svWV z2l)Jye)KY344hLLqr-@@haET7$ zFUizk(8Ihr8Y}Go(rlFeI$VDb3F3loKl-2di^^Q;s&!DkEKU{jut6!*sot27k7%b2N~8W_2>h@|WtOY~y~_Zt2_`2@_{3p>9nE?( z9Q?|ll!}}6hjMb5!#jRbWyOp^V=BhY7(HhEjH-&U6NXh)5Xr6~$tsc)U?vf+9?<7P zdTgXwN;#;H2}vc^rns2)uehhPD>0)r9vYEjyn_~Edl2!3VrkIJ`ifB4{;EC&PSv8X z_EDNT8BU!yXrR)AX$19``bc6yM?vO!Qyz(2>~QL9y&kqdq%h0Q+JK$E=m^!-Cv76InGp|7Lz zfNdD}<(oKgI3f+OJ4q1%y2E-ml)b4>l>CSFM?&Gq6r&jmkLo7?cP_*%Z9b|;0e}3g zel+Yqr&q&kZeyy{a!%j7gZ#<&^=I0L!m>m9>j(O>0Xp15ufIBgKjehA0)qpZqi_Mz z9ERySv=OAGI6E8M!wl#{M4*Vh6=_gQe{vYkN5v3DAMXv!pdnrgleIFxyP>*PCN|+7 zl02E=X{biKcUOBBM(XuV7~)`#$1PA28A5_93&oWvhW|oOz@4x5V7{{^3uZbDe4Tc_ze-^T@f&eMm!a4D)t+kv3EC(t99p=*Ltew z2Ab*-XPzHdUZFNxL-6D26ur~YwS(VN=fPheV1z~7+>?Go42?^F@*8Y$GGH*1UN=(o zcE|}B{z608-vZdSkp#HwC9Ot|6iR{yJMB0vi0g+Tu|ZlMGz>F!*Hg{(vzwl3Vs-6$ zN%HJd0(1r(l7fB&eTD2!dA|sr?{4+^zYkmiaT?0 zPG-@J?96GYQ`44CO~VXu*Sck1wA^^-fEm3hNnRmc$eku}9~iz3hu^LlqP3__2!AEI zE$_$2LZ4A6K^r!4E~)-oLzLEx1I%&zXQ53k&BXfxY1L1L`*`?OYwQV%2%}9Z&>8Q9 zLw21mTpBJIlQ<>uxKd%otA+^p_+L>fX=nzMqyl*TxOim znd1P=ag@O~ewAiRxiJe?@6kH8WH>F-m~!I_A#}kHmZM%W4K@ycLASa$h6R~Zu zv4YkOC^W03TZ4^(5cu_0OtiFWm~kUZ8lhs)bWvUy1GtE?aKX%fg<^8VkGU6;g~3!w z6&Q)9Q8C82(E`dZwRSl$gljX6f0p*UjoJ{nburo`w!Nr=*c!}W?wgF3Uzo{p223ah zy?9@-7;jEsU>z53L7x(@D9G<*0yOr96^jKsY#ggn!ptE?t#q#1SQtXGrJat9&o{TJ z5sq9j$3Wxbx)c(?>B01)*$xMf>cXVevyDL$><%%ZGe6y=?bYE7(7TatM!Sg|Iwa0) z_$)j)v1kDfS9u)MHBT=v+PaK#Xo0ae4CouJftw4A$#7y02ClISjpAlFyjtblQloW- zVmJXW8BFP6%P?~S*@7y4ON~({S&gQdp#!^4oo3BDx22}3zVRN6ok#|<>vj>o*srY6 z?K;Sr{u48~vzM=-Zc*2*VtedaSY6jtgXw2>t~)zmIOA*ZVkm=ca%9v+GVlrrRvgs{ zQe3vFN-sNE$h=94C^0S4Yvof-q9t+pH;K}b!KO6^=SXkE+;$h^E*C|bJkE4L3DaLP zL`bodOyiVN<8)KC2^t!NSh!ke%2h0cqbZsUX#5bB!KD<=2+j9|*kSo+`aaO|nO?o+ z6>W)xCv-^&$<48e;onFIdh1EZxU|vq*MTNiop)AdQLt2pXc?_igX%q#6)wGNmmLn-sjy3f-ZME(#Gxr|&ysS|8|O_4 z3Q+hl*Y?c?(+s%rxq+qtrP_<8>`aaXf#zf8IQXs-CI8?tyjQH-$frwJj+qZ8!r>pnY+GK| zC}Br7uYrcYqNRQFnz>#@qugh%m@V*orAnz4h|sYo=}MM6VTl#YsmSRSyc2ZM)IfG% zQT=RR!=h{#Ecu>`mC`nH<$a;WVM&r&-sQfkP-Mc3HcO(k$zmC&Qe(I(xRNn9w<*R3 z4>sz?O5-9er3wl0uW4XOg2j>5DV_D0X!H)n)DXG0?W_*qgAq@AH5^H>RDvnd5(gQH zmVB6rCF%Y|OHPP{P&OTa{L6Qh!T04-q`Nlg(o- zpM`?sjks_)@*Nt8$A?->(#8pvawTNGZBfG%2{VMBPPR-|z~9>R5m0$6Dom=HYI#=y zKi-e2%dF{^a9V`rV;xFq(hSRZlIeP3rX{8OiN(Bm-LQt)zK#n=Ei^C2<;`|m&V+AS zV2L8BA!dWTfpI8~pAawegiEn95)UcQ)uwj~x7 ztX*PxAG{C8;N46#rj!5vx~3;xb@_AuQp*r{ZcMBdDxZnQRLe5l?k#6StWdbjQbqBe zTxKyy&n&Ziqkxu0W?fi0nWOm%rtS{@h>jNe%^A?L6OGrMW=jQ(|4N+(%Xc`{FlM<$ zgskK0NcjN5u>?yLB=?QBK~|YgYe6$kURezH%_4U&c*_K%S6Cv+#Rr=e*^jOh_s{O; z6nSfg;L8kpKh@C(15Ejq2A=Z0D=Z%>D6cL3!|ZTvr6o#wb0tb*7zuRX&O??;sd|+K zI=_FGTcqDtTW%>JIufs|d@shRz`WKn1^)c0&IG5<>9w$At!069bFJkwtt?@X6~*t8 z);wmJ77FLhcq=wwt}YCIe%z72@5_|A)<+I8I)4m zHp^ZG?ERD7DSh>nB_RZASMkx1+-ey^@^U&QQ5QN;OyI3>vejadcC}jOhrmOx8Z6*l zVmH9Sttu^K?y~#<3v>BMsc5$)Jrr_xML8krUCeW~6~#uv`}=fB@>PtqcaNoC2&h&X z!lfVgTK*jhsh8u?5!z>|4I=}AWgX7BZFf`w%-nCmWX&>sEWyp=G}6ZX7E=g}b@6uT z&_T;py)wny5KNV}ziHW`g8QFxI;Bx>;}cbFN8$sH{>x$wBX4V@*ZyTW9?~T#cFamYz=RDyM$hLm7N%ZRY_60c=AXCL+TV4d66@N7_}NN{-rG) zl!owe5#+@MeQrS_ZIU@58WwK9!6o%~WuqrmosB=JC7!j5DA;;$wci);&Bmt@q-Q+Q z!R;wLS9TUYrkjgTqZ;O8J_eKGe-_aH3*jBo$q@c21vKUIPHANr-==`RFKBgeFd6?G z!DJPmt^{jQ6c1bUyrrAPt9CcyEuUvhQ?RiqNFBA~dS0)heit+!Lci%-Jvv1ge@ScPD4 z2ZhJgCRs_8x4TW9vz@gFQ>UZc3%jZUpWlnmZzgot4`dHX6)ev;Ms)gF$f17`3FRU* zzBzagMG{kwrt^^y-8VW`Rz+|kosXrLct51`)a4UUN8|Ij&Z{i8_5pN3JmlBDgrd#h z?)cp=Qtb#N^>}-UH<;ByG8VC&(*6v-E%a`c^5meHaOtNUeh0pi!H{vq0dr63WC_2+ z496#0bnx?UTmn=c;f-)dkA{w$WQlrctA-uJCui~)PW<{a#vN_3 zTo^R(<8|=j1YRqh^zq9TaD1RfCyk%Sqj~-8KD_%o{}%%<=>z;OrF{7!{dFN96A8~I zo1>)LEj(Jz=uNy9mOtgRz`0HQ@A3@+yf5;_^sXR$GucB-*c&r zI(LvyHp{0A_%{AKC^GXoF!3n1lpW=VYvAEyd==HuA0Ok7sjS&)X~F<}P%HE&=N&J- z(z0RKDL&nd4fNq7ZHM2}toBTTD8G8>OR?D-ppGw8Xr-mBTQ-b6je>CT6z}~1M+}K% zu)OWv#7wuXHY<6>`A6LG|IOVNwH2SEpXU4gAqRQ?-J$#B=p?61>&`~n9}@dNjBbaP z->h2LcAD369qT;{{{$IX?AFtKvQ1vHyXO?IwMPDLRA0VrjgD&XMK_QUlq=!HZEMP~ z_I4QxY@Z3*lxZDRjRALE{U7WrLF?Q}{tTZI*-q}icBR#gbnK`a3p zpEqEl;h#J{hWRIdxC6@w=X+XXqC2FJZelkv>fXQXuCCgSt|%~0i2Dzo+{$k-LE7gm zKji-v_tq~~9tNG|)8WKf-qg{HPN8(lG-06pf)3v<%f4#^Ms_6ELar14ue;I7){R%q zVQ}LtKMFQp;N_JmJIA{+g#mOU8{K|){ar_tN^vPZ8zw%p{JMv?ht2nSYvi?;n$Phq z3NXCS_w1l~`2z2NG4JzP(DNc+3fyg8-PwUWPf|+%c%MHL22Whz%e(d(LOOUreVgYx zJ4me;`9^$2^%u~B!(kdf$JOo}A@^%ukd}YOFHyj)D?C1cWYfeB?k;p? zwAVi8r-$5KnRAubbg7gOv+FmIo^VNJioK zYPzqF*sV4-TTpoFJ3dN{Iw=XC%fQhntKBF&QEXGQrS-S@Jz9*GUea5w0x_^Uau%7< zC)*%@GtWfI4Yf`Pkyb0M>tj{;vK6nFBtw$5DTIB%i5%-0o;LI^W@>U-=`yewtVtjk zZDG=Um$fLYD|$Pf#tiH1KWu

ukjz^Z@MPls@cb4gQa&S8hR0pQ7A+m~hM5KyIv< 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 Illuminate\Support\Facades\Storage::class, 'Str' => Illuminate\Support\Str::class, 'URL' => Illuminate\Support\Facades\URL::class, + 'Utils' => App\Helpers\Utils::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, - ], ]; diff --git a/flexiapi/database/migrations/2021_09_06_132040_add_accounts_tombstones_table.php b/flexiapi/database/migrations/2021_09_06_132040_add_accounts_tombstones_table.php index 43fdcf1..7910f46 100644 --- a/flexiapi/database/migrations/2021_09_06_132040_add_accounts_tombstones_table.php +++ b/flexiapi/database/migrations/2021_09_06_132040_add_accounts_tombstones_table.php @@ -1,4 +1,21 @@ . +*/ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; diff --git a/flexiapi/database/migrations/2021_09_16_120958_add_unique_accounts_passwords_aliases_table.php b/flexiapi/database/migrations/2021_09_16_120958_add_unique_accounts_passwords_aliases_table.php index 07f9188..fa4bce9 100644 --- a/flexiapi/database/migrations/2021_09_16_120958_add_unique_accounts_passwords_aliases_table.php +++ b/flexiapi/database/migrations/2021_09_16_120958_add_unique_accounts_passwords_aliases_table.php @@ -1,4 +1,21 @@ . +*/ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; diff --git a/flexiapi/public/css/charts.css b/flexiapi/public/css/charts.css new file mode 100644 index 0000000..01fde53 --- /dev/null +++ b/flexiapi/public/css/charts.css @@ -0,0 +1,77 @@ +.legend { + margin-bottom: 1rem; +} + +.legend div:before { + content: ''; + display: inline-block; + width: 1rem; + height: 1rem; + vertical-align: middle; + background-color: gray; + margin: 0 0.5rem; + opacity: 0.5; +} + +.columns { + display: flex; + height: 600px; + align-items: stretch; + border: 1px solid #CCC; + margin-bottom: 10rem; +} + +.columns .column { + flex: 1; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-end; + position: relative; +} + +.columns .column .bar { + border-right: 1px solid white; + font-size: 0.5rem; + text-align: center; + position: relative; + opacity: 0.5; +} + +.columns .column::after { + display: block; + content: attr(data-value); + position: absolute; + top: calc(100% + 1rem); + writing-mode: vertical-rl; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.columns .column .bar.activated, +.legend div.activated:before { + opacity: 1; +} + +.columns .column .bar.first, +.legend .first:before { + background-color: salmon; +} + +.columns .column .bar.second, +.legend .second:before { + background-color: seagreen; +} + +.columns .column .bar::after { + display: block; + content: attr(data-value); + color: white; + position: absolute; + font-size: 0.75rem; + line-height: 1rem; + left: 0rem; + top: calc(50% - 0.5rem); + width: 100%; +} \ No newline at end of file diff --git a/flexiapi/resources/views/account/documentation_markdown.blade.php b/flexiapi/resources/views/account/documentation_markdown.blade.php index b4cbe1e..1e84962 100644 --- a/flexiapi/resources/views/account/documentation_markdown.blade.php +++ b/flexiapi/resources/views/account/documentation_markdown.blade.php @@ -84,6 +84,14 @@ Finally the account page allows you to provision the account, using a QR Code or The provisioning link can be generated and refreshed from this page as well. +### Create and edit an account + +Administrators can create and edit accounts directly from the admin panel. + ### Delete an account -The deletion of an account is definitive, all the database related data (password, aliases…) will be destroyed after the deletion. \ No newline at end of file +The deletion of an account is definitive, all the database related data (password, aliases…) will be destroyed after the deletion. + +## Statistics + +The statistics panel show registrations statistics based on their type (mobile and email based registration) and their activations states. \ No newline at end of file diff --git a/flexiapi/resources/views/account/panel.blade.php b/flexiapi/resources/views/account/panel.blade.php index 2d6c05f..909853f 100644 --- a/flexiapi/resources/views/account/panel.blade.php +++ b/flexiapi/resources/views/account/panel.blade.php @@ -54,6 +54,12 @@

Manage the Flexisip accounts

+ +
+
Statistics
+
+

Show some registration statistics

+
API Key
diff --git a/flexiapi/resources/views/admin/statistics/parts/columns.blade.php b/flexiapi/resources/views/admin/statistics/parts/columns.blade.php new file mode 100644 index 0000000..b62f166 --- /dev/null +++ b/flexiapi/resources/views/admin/statistics/parts/columns.blade.php @@ -0,0 +1,12 @@ +
+
+
+
\ No newline at end of file diff --git a/flexiapi/resources/views/admin/statistics/parts/legend.blade.php b/flexiapi/resources/views/admin/statistics/parts/legend.blade.php new file mode 100644 index 0000000..b250ea4 --- /dev/null +++ b/flexiapi/resources/views/admin/statistics/parts/legend.blade.php @@ -0,0 +1,6 @@ +
+
Unactivated phones
+
Activated phones
+
Unactivated emails
+
Activated emails
+
\ No newline at end of file diff --git a/flexiapi/resources/views/admin/statistics/show_day.blade.php b/flexiapi/resources/views/admin/statistics/show_day.blade.php new file mode 100644 index 0000000..75f5806 --- /dev/null +++ b/flexiapi/resources/views/admin/statistics/show_day.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.account') + +@section('breadcrumb') + +@endsection + +@section('content') + + + +

Statistics

+ +@include('admin.statistics.parts.legend') + +

Day

+ +
+ @foreach ($day as $key => $hour) +
+ @include('admin.statistics.parts.columns', ['slice' => $hour, 'max' => $max_day]) +
+ @endforeach +
+ +@endsection \ No newline at end of file diff --git a/flexiapi/resources/views/admin/statistics/show_month.blade.php b/flexiapi/resources/views/admin/statistics/show_month.blade.php new file mode 100644 index 0000000..2cd07a4 --- /dev/null +++ b/flexiapi/resources/views/admin/statistics/show_month.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.account') + +@section('breadcrumb') + +@endsection + +@section('content') + + + +

Statistics

+ +@include('admin.statistics.parts.legend') + +

Month

+ +
+@foreach ($month as $key => $day) +
+ @include('admin.statistics.parts.columns', ['slice' => $day, 'max' => $max_month]) +
+@endforeach +
+ +@endsection \ No newline at end of file diff --git a/flexiapi/resources/views/admin/statistics/show_week.blade.php b/flexiapi/resources/views/admin/statistics/show_week.blade.php new file mode 100644 index 0000000..80797ed --- /dev/null +++ b/flexiapi/resources/views/admin/statistics/show_week.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.account') + +@section('breadcrumb') + +@endsection + +@section('content') + + + +

Statistics

+ +@include('admin.statistics.parts.legend') + +

Week

+ +
+@foreach ($week as $key => $day) +
+ @include('admin.statistics.parts.columns', ['slice' => $day, 'max' => $max_week]) +
+@endforeach +
+ +@endsection \ No newline at end of file diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index 8ae1c70..dd176f0 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -186,6 +186,17 @@ Activate an account. #### `GET /accounts/{id}/deactivate` Deactivate an account. +### Statistics + +#### `GET /statistics/day` +Retrieve registrations statistics for 24 hours. + +#### `GET /statistics/week` +Retrieve registrations statistics for a week. + +#### `GET /statistics/month` +Retrieve registrations statistics for a month. + # Provisioning When an account is having an available `confirmation_key` it can be provisioned using the two following URL. diff --git a/flexiapi/resources/views/layouts/base.blade.php b/flexiapi/resources/views/layouts/base.blade.php index 55d0602..028de06 100644 --- a/flexiapi/resources/views/layouts/base.blade.php +++ b/flexiapi/resources/views/layouts/base.blade.php @@ -13,6 +13,7 @@ @else @endif + @endif diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index adbed91..0dc3c85 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -33,6 +33,10 @@ Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmai Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone'); Route::group(['middleware' => ['auth.digest_or_key']], function () { + Route::get('statistic/month', 'Api\StatisticController@month'); + Route::get('statistic/week', 'Api\StatisticController@week'); + Route::get('statistic/day', 'Api\StatisticController@day'); + Route::get('accounts/me', 'Api\AccountController@show'); Route::delete('accounts/me', 'Api\AccountController@delete'); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index d6ada5b..b81daba 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -71,6 +71,10 @@ Route::group(['middleware' => 'auth'], function () { Route::group(['middleware' => 'auth.admin'], function () { Route::post('admin/api_key', 'Admin\AccountController@generateApiKey')->name('admin.api_key.generate'); + Route::get('admin/statistics/day', 'Admin\StatisticsController@showDay')->name('admin.statistics.show.day'); + Route::get('admin/statistics/week', 'Admin\StatisticsController@showWeek')->name('admin.statistics.show.week'); + Route::get('admin/statistics/month', 'Admin\StatisticsController@showMonth')->name('admin.statistics.show.month'); + Route::get('admin/accounts/{account}/show', 'Admin\AccountController@show')->name('admin.account.show'); Route::get('admin/accounts/{account}/activate', 'Admin\AccountController@activate')->name('admin.account.activate'); diff --git a/flexisip-account-manager.spec b/flexisip-account-manager.spec index 77647df..7bd4bec 100644 --- a/flexisip-account-manager.spec +++ b/flexisip-account-manager.spec @@ -8,7 +8,7 @@ #%define _datadir %{_datarootdir} #%define _docdir %{_datadir}/doc -%define build_number 105 +%define build_number 106 %define var_dir /var/opt/belledonne-communications %define opt_dir /opt/belledonne-communications/share/flexisip-account-manager @@ -64,12 +64,16 @@ cp README* "$RPM_BUILD_ROOT%{opt_dir}/" mkdir -p "$RPM_BUILD_ROOT/etc/flexisip-account-manager" cp -R conf/* "$RPM_BUILD_ROOT/etc/flexisip-account-manager/" +mkdir -p $RPM_BUILD_ROOT/etc/cron.daily + %if %{with deb} mkdir -p $RPM_BUILD_ROOT/etc/apache2/conf-available cp httpd/flexisip-account-manager.conf "$RPM_BUILD_ROOT/etc/apache2/conf-available/" + cp cron/flexiapi.debian "$RPM_BUILD_ROOT/etc/cron.daily/" %else mkdir -p $RPM_BUILD_ROOT/opt/rh/httpd24/root/etc/httpd/conf.d cp httpd/flexisip-account-manager.conf "$RPM_BUILD_ROOT/opt/rh/httpd24/root/etc/httpd/conf.d/" + cp cron/flexiapi.redhat "$RPM_BUILD_ROOT/etc/cron.daily/" %endif # POST INSTALLATION @@ -168,15 +172,19 @@ fi %if %{with deb} %config(noreplace) /etc/apache2/conf-available/flexisip-account-manager.conf + %config(noreplace) /etc/cron.daily/flexiapi.debian %else %config(noreplace) /opt/rh/httpd24/root/etc/httpd/conf.d/flexisip-account-manager.conf + %config(noreplace) /etc/cron.daily/flexiapi.redhat %endif %clean rm -rf $RPM_BUILD_ROOT %changelog -* Tue Jan 5 2020 Timothée Jaussoin +* Tue Sep 28 2021 Timothée Jaussoin +- Install cron scripts +* Sun Jan 5 2020 Timothée Jaussoin - Import and configure the new API package * Thu Jul 4 2019 Sylvain Berfini - New files layout