diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index e28687b..9eb056b 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -41,6 +41,7 @@ class Account extends Authenticatable protected $casts = [ 'activated' => 'boolean', ]; + protected $fillable = ['username', 'domain', 'email']; public static $dtmfProtocols = ['sipinfo' => 'SIPInfo', 'rfc2833' => 'RFC2833', 'sipmessage' => 'SIP Message']; diff --git a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php index ca08b03..c289b73 100644 --- a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php +++ b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php @@ -63,7 +63,7 @@ class AuthenticateController extends Controller } if (!$account) { - return redirect()->back()->withErrors(['authentication' => 'The account doesn\'t exists']); + return redirect()->back()->withErrors(['authentication' => 'Wrong username or password']); } // Try out the passwords diff --git a/flexiapi/app/Http/Controllers/Account/PasswordController.php b/flexiapi/app/Http/Controllers/Account/PasswordController.php index c1cbf5d..d00d40d 100644 --- a/flexiapi/app/Http/Controllers/Account/PasswordController.php +++ b/flexiapi/app/Http/Controllers/Account/PasswordController.php @@ -45,9 +45,7 @@ class PasswordController extends Controller $account->activated = true; $account->save(); - $algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5'; - - $account->updatePassword($request->get('password'), $algorithm); + $account->updatePassword($request->get('password'), 'SHA-256'); if ($account->passwords()->count() > 0) { Log::channel('events')->info('Web: Password changed', ['id' => $account->identifier]); diff --git a/flexiapi/app/Http/Controllers/Account/RecoveryController.php b/flexiapi/app/Http/Controllers/Account/RecoveryController.php index 1d94730..b7cf4b4 100644 --- a/flexiapi/app/Http/Controllers/Account/RecoveryController.php +++ b/flexiapi/app/Http/Controllers/Account/RecoveryController.php @@ -36,6 +36,8 @@ class RecoveryController extends Controller 'g-recaptcha-response' => captchaConfigured() ? 'required|captcha' : '', ]; + $account = null; + if ($request->get('email')) { if (config('app.account_email_unique') == false) { $rules['username'] = 'required'; @@ -94,12 +96,18 @@ class RecoveryController extends Controller { $request->validate([ 'account_id' => 'required', - 'code' => 'required|digits:4' + 'number_1' => 'required|digits:1', + 'number_2' => 'required|digits:1', + 'number_3' => 'required|digits:1', + 'number_4' => 'required|digits:1' ]); + $code = $request->get('number_1') . $request->get('number_2') . $request->get('number_3') . $request->get('number_4'); + + $account = Account::where('id', Crypt::decryptString($request->get('account_id')))->firstOrFail(); - if ($account->recovery_code != $request->get('code')) { + if ($account->recovery_code != $code) { return redirect()->back()->withErrors([ 'code' => 'Wrong code' ]); diff --git a/flexiapi/app/Http/Controllers/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Admin/AccountController.php index 0ef7cec..b9319b6 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountController.php @@ -97,6 +97,9 @@ class AccountController extends Controller $account->phone = $request->get('phone'); $account->fillPassword($request); + $account->refresh(); + $account->setRole($request->get('role')); + Log::channel('events')->info('Web Admin: Account created', ['id' => $account->identifier]); return redirect()->route('admin.account.edit', $account->id); diff --git a/flexiapi/app/Http/Controllers/Admin/AccountImportController.php b/flexiapi/app/Http/Controllers/Admin/AccountImportController.php new file mode 100644 index 0000000..4530d61 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Admin/AccountImportController.php @@ -0,0 +1,226 @@ +errors = collect(); + } + + public function create(Request $request) + { + return view('admin.account.import.create', [ + 'domains' => Account::select('domain')->distinct()->get()->pluck('domain') + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'csv' => ['required', File::types(['csv', 'txt'])], + 'domain' => 'required|exists:accounts' + ]); + + $lines = $this->csvToCollection($request->file('csv')); + + /** + * Error checking + */ + + // Usernames + + $existingUsernames = Account::where('domain', $request->get('domain')) + ->whereIn('username', $lines->pluck('username')->all()) + ->pluck('username'); + + if ($existingUsernames->isNotEmpty()) { + $this->errors['Those usernames already exists'] = $existingUsernames->join(', ', ' and '); + } + + if ($duplicates = $lines->pluck('username')->duplicates()) { + if ($duplicates->isNotEmpty()) { + $this->errors['Those usernames are declared several times'] = $duplicates->join(', ', ' and '); + } + } + + if ($lines->pluck('username')->contains(function ($value) { + return strlen($value) < 2; + })) { + $this->errors['Some usernames are shorter than expected'] = ''; + } + + // Passwords + + if ($lines->pluck('password')->contains(function ($value) { + return strlen($value) < 6; + })) { + $this->errors['Some passwords are shorter than expected'] = ''; + } + + // Roles + + if ($lines->pluck('role')->contains(function ($value) { + return !in_array($value, ['admin', 'user']); + })) { + $this->errors['Some roles are not correct'] = ''; + } + + // Status + + if ($lines->pluck('status')->contains(function ($value) { + return !in_array($value, ['active', 'inactive']); + })) { + $this->errors['Some status are not correct'] = ''; + } + + // Phones + + if ($phones = $lines->pluck('phone')->filter(function ($value) { + return strlen($value) > 2 && substr($value, 0, 1) != '+'; + })) { + if ($phones->isNotEmpty()) { + $this->errors['Some phone numbers are not correct'] = $phones->join(', ', ' and '); + } + } + + $existingPhones = Alias::whereIn('alias', $lines->pluck('phone')->all()) + ->pluck('alias'); + + if ($existingPhones->isNotEmpty()) { + $this->errors['Those phones numbers already exists'] = $existingPhones->join(', ', ' and '); + } + + // Emails + + if ($emails = $lines->pluck('email')->filter(function ($value) { + return !filter_var($value, FILTER_VALIDATE_EMAIL); + })) { + if ($emails->isNotEmpty()) { + $this->errors['Some emails are not correct'] = $emails->join(', ', ' and '); + } + } + + $existingEmails = Account::whereIn('email', $lines->pluck('email')->all()) + ->pluck('email'); + + if ($existingEmails->isNotEmpty()) { + $this->errors['Those emails numbers already exists'] = $existingEmails->join(', ', ' and '); + } + + if ($emails = $lines->pluck('email')->duplicates()) { + if ($emails->isNotEmpty()) { + $this->errors['Those emails are declared several times'] = $emails->join(', ', ' and '); + } + } + + $filePath = $this->errors->isEmpty() + ? Storage::putFile($this->importDirectory, $request->file('csv')) + : null; + + return view('admin.account.import.check', [ + 'linesCount' => $lines->count(), + 'errors' => $this->errors, + 'domain' => $request->get('domain'), + 'filePath' => $filePath + ]); + } + + public function handle(Request $request) + { + $request->validate([ + 'file_path' => 'required', + 'domain' => 'required|exists:accounts' + ]); + + $lines = $this->csvToCollection(storage_path('app/' . $request->get('file_path'))); + + $accounts = []; + $now = \Carbon\Carbon::now(); + + $admins = $phones = []; + + foreach ($lines as $line) { + if ($line->role == 'admin') { + array_push($admins, $line->username); + } + + if (!empty($line->phone)) { + $phones[$line->username] = $line->phone; + } + + array_push($accounts, [ + 'username' => $line->username, + 'domain' => $request->get('domain'), + 'email' => $line->email, + 'activated' => $line->status == 'active', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'CSV Import', + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + Account::insert($accounts); + + // Set admins accounts + foreach ($admins as $username) { + $account = Account::where('username', $username) + ->where('domain', $request->get('domain')) + ->first(); + $account->admin = true; + } + + // Set admins accounts + foreach ($phones as $username => $phone) { + $account = Account::where('username', $username) + ->where('domain', $request->get('domain')) + ->first(); + $account->phone = $phone; + } + + return redirect()->route('admin.account.index'); + } + + private function csvToCollection($file): Collection + { + $lines = collect(); + $csv = fopen($file, 'r'); + + $i = 1; + while (!feof($csv)) { + if ($line = fgetcsv($csv, 1000, ',')) { + $lines->push((object)[ + 'line' => $i, + 'username' => $line[0], + 'password' => $line[1], + 'role' => $line[2], + 'status' => $line[3], + 'phone' => $line[4], + 'email' => $line[5], + ]); + + $i++; + } + } + + fclose($csv); + + $lines->shift(); + + return $lines; + } +} diff --git a/flexiapi/public/css/far.css b/flexiapi/public/css/far.css index 93b7237..f129338 100644 --- a/flexiapi/public/css/far.css +++ b/flexiapi/public/css/far.css @@ -106,6 +106,7 @@ body.show_menu { p, a, ul li, +ol li, pre { font-size: 1.5rem; color: var(--second-7); @@ -124,11 +125,16 @@ pre code { font-size: 1.3rem; } -ul li { +ul li, +ol li { margin-left: 2rem; list-style-type: disc; } +ol li { + list-style-type: decimal; +} + ul li ul li { list-style-type: circle; } @@ -279,20 +285,12 @@ header nav a#menu { header nav a#menu:after { display: block; - font-family: 'Material Icons'; + font-family: 'Material Icons Outlined'; content: "\e5d2"; font-size: 3rem; } -body.show_menu header nav a#menu:after { - content: "\e5cd"; -} - -header nav a#logo span { - margin-left: 1.5rem; -} - -@media screen and (max-width: 800px) { +body.show_menu header nav a#menu:after {font-family: 'Material Icons' header nav a#logo { position: absolute; left: calc(50% - 1.5rem); @@ -314,6 +312,7 @@ header nav a#logo::before { display: block; border-radius: 1rem; box-shadow: 0 0 2rem rgba(0, 0, 0, 0.2); + margin-right: 2rem; } header nav a.oppose { @@ -619,7 +618,7 @@ table tr.empty td { table tr.empty td:before { content: '\e5c9'; - font-family: 'Material Icons'; + font-family: 'Material Icons Outlined'; font-size: 8rem; color: var(--second-4); display: block; @@ -753,6 +752,7 @@ select.list_toggle { display: inline-block; font-size: 1.5rem; line-height: 2rem; + margin-left: 0; } .breadcrumb li a { @@ -769,4 +769,54 @@ select.list_toggle { font-size: 1rem; line-height: 2rem; display: inline-block; +} + +/* Steps */ + +ol.steps { + counter-reset: css-counter 0; + padding-top: 6.5rem; +} + +ol.steps li { + counter-increment: css-counter 1; + display: inline-block; + position: relative; + font-weight: 600; + color: var(--grey-5); +} + +ol.steps li + li { + margin-left: 9rem; +} + +ol.steps li + li:after { + display: block; + border-top: 1px solid var(--grey-3); + content: ""; + width: 7rem; + position: absolute; + top: -3rem; + left: -8rem; +} + +ol.steps li:before { + content: counter(css-counter); + display: block; + font-size: 2rem; + font-weight: 300; + border-radius: 3rem; + line-height: 5rem; + width: 5rem; + text-align: center; + border: 1px solid var(--grey-3); + position: absolute; + left: calc(50% - 2.5rem - 1px); + bottom: 3rem; +} + +ol.steps li.active:before { + background-color: var(--main-5); + border-color: var(--main-5); + color: white; } \ No newline at end of file diff --git a/flexiapi/public/css/form.css b/flexiapi/public/css/form.css index 397e2c3..951d5b4 100644 --- a/flexiapi/public/css/form.css +++ b/flexiapi/public/css/form.css @@ -65,16 +65,18 @@ p .btn { .btn.btn-tertiary { background-color: var(--main-1); - border-color: transparent; + border-color: var(--main-1); color: var(--main-5); } .btn.btn-tertiary:hover { background-color: var(--main-2); + border-color: var(--main-2); } .btn.btn-tertiary:active { background-color: var(--main-3); + border-color: var(--main-3); } form { @@ -129,7 +131,7 @@ form h2 { grid-column: 1/-1; } -form .disabled { +form .disabled:not(a) { opacity: 0.5; pointer-events: none; filter: blur(0.25rem); @@ -205,7 +207,7 @@ form div.checkbox { form div.search:after, form div.select:after { - font-family: 'Material Icons'; + font-family: 'Material Icons Outlined'; content: "\e5cf"; display: block; font-size: 2.5rem; diff --git a/flexiapi/resources/views/account/delete.blade.php b/flexiapi/resources/views/account/delete.blade.php index 3cf2e47..ab1ca4d 100644 --- a/flexiapi/resources/views/account/delete.blade.php +++ b/flexiapi/resources/views/account/delete.blade.php @@ -1,23 +1,36 @@ @extends('layouts.main') +@section('breadcrumb') + +@endsection + @section('content') -

Delete my account

+
+

delete Delete my account

-
-@csrf + Cancel + +
- @method('delete') + + @csrf + @method('delete') -

You are going to permanently delete your account.

-

Please enter your complete username to confirm: {{ $account->identifier }}.

+
+

You are going to permanently delete your account.

+

Please enter your complete username to confirm: {{ $account->identifier }}.

+
-
- - -
+
+ + +
- +
- + + +
+
@endsection diff --git a/flexiapi/resources/views/account/login.blade.php b/flexiapi/resources/views/account/login.blade.php index e6a472c..30f7cf3 100644 --- a/flexiapi/resources/views/account/login.blade.php +++ b/flexiapi/resources/views/account/login.blade.php @@ -20,6 +20,7 @@ value="{{ old('username') }}"> @endif + @include('parts.errors', ['name' => 'authentication'])
diff --git a/flexiapi/resources/views/account/password.blade.php b/flexiapi/resources/views/account/password.blade.php index 7f730b7..cc5a02a 100644 --- a/flexiapi/resources/views/account/password.blade.php +++ b/flexiapi/resources/views/account/password.blade.php @@ -1,13 +1,22 @@ @extends('layouts.main') -@section('content') - @if ($account->passwords()->count() > 0) -

Change my account password

- @else -

Set my account password

- @endif +@section('breadcrumb') + +@endsection -
+@section('content') +
+ @if ($account->passwords()->count() > 0) +

lock Change password

+ @else +

lock Set password

+ @endif + + Cancel + +
+ + @csrf
@@ -18,12 +27,5 @@
-
- - -
- -
@endsection diff --git a/flexiapi/resources/views/account/recovery/confirm.blade.php b/flexiapi/resources/views/account/recovery/confirm.blade.php index 869cb28..47147e7 100644 --- a/flexiapi/resources/views/account/recovery/confirm.blade.php +++ b/flexiapi/resources/views/account/recovery/confirm.blade.php @@ -9,8 +9,12 @@

Enter the pin code you received to recover your account.

- - + + + + + @include('parts.errors', ['name' => 'code']) +
diff --git a/flexiapi/resources/views/admin/account/create_edit.blade.php b/flexiapi/resources/views/admin/account/create_edit.blade.php index c49bb32..08b92eb 100644 --- a/flexiapi/resources/views/admin/account/create_edit.blade.php +++ b/flexiapi/resources/views/admin/account/create_edit.blade.php @@ -19,7 +19,6 @@

Updated on {{ $account->updated_at->format('d/m/Y') }} - @include('parts.tabs', [ 'items' => [ route('admin.account.edit', $account->id, ['type' => 'messages']) => 'Information', @@ -42,7 +41,7 @@

Connexion

id) readonly @endif> + value="@if ($account->id){{ $account->username }}@else{{ old('username') }}@endif" @if ($account->id) readonly @endif> @include('parts.errors', ['name' => 'username'])
@@ -54,32 +53,32 @@
- + @include('parts.errors', ['name' => 'display_name'])
- + id)required @endif> @include('parts.errors', ['name' => 'password'])
- + id)required @endif> @include('parts.errors', ['name' => 'password_confirmation'])
- + @include('parts.errors', ['name' => 'email'])
- + @include('parts.errors', ['name' => 'phone'])
@@ -242,10 +241,12 @@ {{ $type->key }} -
-@csrf -@method('delete') - + + @csrf + @method('delete') +
diff --git a/flexiapi/resources/views/admin/account/delete.blade.php b/flexiapi/resources/views/admin/account/delete.blade.php index da04640..87094f4 100644 --- a/flexiapi/resources/views/admin/account/delete.blade.php +++ b/flexiapi/resources/views/admin/account/delete.blade.php @@ -8,24 +8,23 @@ @endsection @section('content') -

Delete an account

+
+

delete Delete an account

-
-@csrf - - @method('delete') - -
-

You are going to permanently delete the following account. Please confirm your action.
- {{ $account->identifier }} -

- - -
-
- - -
+ Cancel + +
+ + @csrf + @method('delete') +
+

You are going to permanently delete the following account. Please confirm your action.
+ {{ $account->identifier }} +

+ +
+
+
@endsection diff --git a/flexiapi/resources/views/admin/account/import/check.blade.php b/flexiapi/resources/views/admin/account/import/check.blade.php new file mode 100644 index 0000000..28f0516 --- /dev/null +++ b/flexiapi/resources/views/admin/account/import/check.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.main') + +@section('breadcrumb') + + +@endsection + +@section('content') +
+

people Import accounts

+ Cancel + + Previous +
+ @csrf + + + + publish + Import + +
+
+ +
+
    +
  1. Select data file
  2. +
  3. Import data
  4. +
+ +

{{ $linesCount }} accounts will be imported for the {{ $domain }} domain

+ + @if ($errors->isNotEmpty()) +
+ +

Errors

+ + @foreach ($errors as $title => $body) +

{{ $title }} {{ $body }}

+ @endforeach + @endif +
+@endsection diff --git a/flexiapi/resources/views/admin/account/import/create.blade.php b/flexiapi/resources/views/admin/account/import/create.blade.php new file mode 100644 index 0000000..2322937 --- /dev/null +++ b/flexiapi/resources/views/admin/account/import/create.blade.php @@ -0,0 +1,58 @@ +@extends('layouts.main') + +@section('breadcrumb') + + +@endsection + +@section('content') +
+

people Import accounts

+ Cancel + +
+ +
+
    +
  1. Select data file
  2. +
  3. Import data
  4. +
+ +

Use this existing (.csv) template or create your own csv file.

+ +

+ This file MUST be in csv format and contain at least the following information: +

+
    +
  1. The first line contains the labels
  2. +
  3. Username*
  4. +
  5. Password* (6 characters minimum)
  6. +
  7. Role* (admin or user)
  8. +
  9. Statuts* (active, inactive)
  10. +
  11. Phone number, must start with a + if set
  12. +
+ +
+ +
+ @csrf +
+ + @include('parts.errors', ['name' => 'csv']) + +
+
+ +
+ + +
+
+
+@endsection diff --git a/flexiapi/resources/views/admin/account/index.blade.php b/flexiapi/resources/views/admin/account/index.blade.php index 5e18bd2..379abcf 100644 --- a/flexiapi/resources/views/admin/account/index.blade.php +++ b/flexiapi/resources/views/admin/account/index.blade.php @@ -9,7 +9,11 @@ @section('content')

people Accounts

- + + publish + Import Accounts + + add_circle New Account @@ -75,7 +79,7 @@ @if ($accounts->isEmpty()) - No Contacts Lists + No Accounts @endif @foreach ($accounts as $account) diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index cd5e93c..fb06ad4 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -30,6 +30,7 @@ use App\Http\Controllers\Admin\AccountContactController; use App\Http\Controllers\Admin\AccountDeviceController; use App\Http\Controllers\Admin\AccountTypeController; use App\Http\Controllers\Admin\AccountController as AdminAccountController; +use App\Http\Controllers\Admin\AccountImportController; use App\Http\Controllers\Admin\ContactsListController; use App\Http\Controllers\Admin\ContactsListContactController; use App\Http\Controllers\Admin\StatisticsController; @@ -153,6 +154,12 @@ if (config('app.web_panel')) { Route::post('{account_id}/contacts_lists', 'contactsListAdd')->name('contacts_lists.attach'); }); + Route::name('import.')->prefix('import')->controller(AccountImportController::class)->group(function () { + Route::get('/', 'create')->name('create'); + Route::post('/', 'store')->name('store'); + Route::post('handle', 'handle')->name('handle'); + }); + Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () { Route::get('/', 'index')->name('index'); Route::get('create', 'create')->name('create');