Compare commits

..

76 commits

Author SHA1 Message Date
Timothée Jaussoin
6a1f3471d6 Complete the INSTALL and CHANGELOG documentations 2026-01-14 11:00:20 +01:00
Timothée Jaussoin
85c939b4da Fix FLEXIAPI-431 Complete missing admin account dictionnary documentation 2026-01-12 10:41:45 +00:00
Timothée Jaussoin
f883c3cee7 Fix FLEXIAPI-430 Handle empty string in JWT_SIP_IDENTIFIER and fallback to sip_identity 2026-01-06 15:44:36 +01:00
Timothée Jaussoin
06dc357524 Fix FLEXIAPI-427 Add missing file 2026-01-05 09:11:06 +01:00
Timothée Jaussoin
4d601c4a9c Fix FLEXIAPI-366 Add voicemails endpoints 2025-12-11 16:27:32 +00:00
Timothée Jaussoin
09d386a303 Fix FLEXIAPI-424 Logout when the password is correctly changed 2025-12-09 16:14:09 +00:00
Timothée Jaussoin
0740bd0425 Fix FLEXIAPI-369 Refresh the getCountryCodes() list and add a console script... 2025-12-09 15:31:18 +00:00
Timothée Jaussoin
d179d0f6df Fix FLEXIAPI-415 Add endpoints to activate/deactivate phone countries 2025-12-09 16:15:06 +01:00
Timothée Jaussoin
25ddd330c1 Fix FLEXIAPI-408 Adjustments to allow custom JWT tests and hooks 2025-12-09 15:04:51 +01:00
Timothée Jaussoin
e0f33da4ac Fix FLEXIAPI-411 Add a temporary toggle to hide the login page counter globally 2025-12-04 16:29:04 +01:00
Timothée Jaussoin
ae4a651f2a Fix FLEXIAPI-417 Fix accounts:create-admin-test to fit with spaces:create-update, wrong parameter 2025-12-04 12:01:10 +01:00
Timothée Jaussoin
593d7ce5c0 Fix FLEXIAPI-400 Scope the API accounts endpoints per space 2025-12-01 17:00:11 +01:00
Timothée Jaussoin
6fcde1b467 Fix FLEXIAPI-416 Respect the Spaces columns contraints in LiblinphoneTesterAccoutSeeder 2025-12-01 11:27:47 +01:00
Timothée Jaussoin
789c27f654 Fix FLEXIAPI-417 Fix accounts:create-admin-test to fit with spaces:create-update 2025-11-27 17:56:35 +01:00
Timothée Jaussoin
a53910d364 Fix FLEXIAPI-386 Move the Contacts Lists in Spaces 2025-11-25 16:56:03 +01:00
Timothée Jaussoin
abcc9c1c7b Fix FLEXIAPI-412 Restrict the default messages statistics graph to the space... 2025-11-25 12:43:55 +01:00
Timothée Jaussoin
26aaab2f07 Fix FLEXIAPI-406 Add an artisan console script to clear statistics after n days 2025-11-18 16:15:52 +00:00
Timothée Jaussoin
fe265a972d Fix FLEXIAPI-410 Modernize the Middleware and application stack to fit with... 2025-11-12 14:50:07 +00:00
Timothée Jaussoin
98d9d76225 Fix FLEXIAPI-404 Prevent the account domain to be reset during creation on error 2025-10-15 11:15:32 +02:00
Timothée Jaussoin
9aeeb0fa73 Fix FLEXIAPI-402 Handle empty emails cases when importing accounts, handle... 2025-10-14 09:24:28 +00:00
Timothée Jaussoin
57e09cc4de Fix FLEXIAPI-403 Fix #14183 Cast the DotEnv setting to integer before passing them to Carbon 2025-10-14 10:22:42 +02:00
Timothée Jaussoin
ed28e8fe55 Fix FLEXIAPI-401 Use a Space scoped reset password URL 2025-10-13 14:31:48 +02:00
Timothée Jaussoin
689140a553 Fix FLEXIAPI-398 List all the availables Spaces domains when importing, fix... 2025-10-08 09:40:41 +00:00
Timothée Jaussoin
7221d55ff8 Fix FLEXIAPI-396 Remove the CHANGELOG.md file (redundant with the Git history)... 2025-10-07 09:26:17 +00:00
Timothée Jaussoin
0e3b0d36d8 Fix FLEXIAPI-397 Fix Carddav Spaces creation and CommaList validation 2025-10-06 11:43:52 +02:00
Timothée Jaussoin
10cdbb4b6a Fix FLEXIAPI-393 Autofill the digits when pasting a code 2025-09-30 11:56:33 +02:00
Timothée Jaussoin
b0de4841f6 Fix FLEXIAPI-395 Remove config()->set('app.sip_domain') and directly use the correct domain 2025-09-29 15:25:26 +02:00
Timothée Jaussoin
a98c8764d5 Fix FLEXIAPI-392 Fix the recover_by_code view and use the account space object 2025-09-29 06:59:52 +00:00
Timothée Jaussoin
fbf47fc9c9 Fix FLEXIAPI-391 Add missing account view attribute in the actions.delete view 2025-09-24 14:51:48 +02:00
Timothée Jaussoin
6240f81f14 Fix FLEXIAPI-387 Ensure that INSTANCE_CONFIRMED_REGISTRATION_TEXT et... 2025-09-18 15:59:41 +02:00
Timothée Jaussoin
edbe49d404 Fix FLEXIAPI-385 Use domains and not hosts in the EmailServer endpoints as... 2025-09-11 13:17:44 +00:00
Timothée Jaussoin
d8f0c47d8f Fix FLEXIAPI-384 Allow carddav_user_credentials to be set and use the correct... 2025-09-11 08:42:15 +00:00
Timothée Jaussoin
6770e198d9 Fix FLEXIAPI-382 Package for Rocky 10 2025-09-09 09:24:44 +02:00
Timothée Jaussoin
5f22c8c862 Fix FLEXIAPI-381 Remove Remi in the Rocky9 pipeline 2025-09-08 13:55:50 +00:00
Timothée Jaussoin
a56de2e93a Fix FLEXIAPI-380 Fix CardDav documentation 2025-09-01 15:07:54 +02:00
Timothée Jaussoin
38f0120ecc Fix FLEXIAPI-377 Don't reset authInfoIndex in the Provisioning 2025-09-01 10:23:40 +02:00
Timothée Jaussoin
abe67c9734 Fix FLEXIAPI-378 Return a valid JSON containing the vCard and not the raw... 2025-08-28 09:08:04 +00:00
Timothée Jaussoin
ee2c9fed8f Fix FLEXIAPI-375 Fix VcardsStorage table UUID size, recover the UUID from the stored vCard 2025-08-27 16:29:47 +02:00
Timothée Jaussoin
e0a9b75923 Fix FLEXIAPI-376 Rename domain to realm in CardDav credentials 2025-08-27 15:36:11 +02:00
Timothée Jaussoin
2a3634d461 Fix FLEXIAPI-374 Improve CardDav credentials form 2025-08-27 08:25:38 +00:00
Timothée Jaussoin
2ec4f488b6 Fix FLEXIAPI-372 Remove SESSION_DRIVER and CACHE_DRIVER and enforce them to file 2025-08-26 14:10:21 +02:00
Timothée Jaussoin
60df61d508 Fix FLEXIAPI-359 Add CardDav servers support in the spaces 2025-08-26 09:03:50 +00:00
Timothée Jaussoin
9a9b8ab34e Fix FLEXIAPI-371 Add documentation for the Wizard page 2025-08-18 16:55:13 +02:00
Timothée Jaussoin
a876a8cf82 Fix FLEXIAPI-361 Prepare the 2.0 release 2025-08-18 11:23:42 +02:00
Timothée Jaussoin
a5eeb06055 Upgrade to PHP 8.2 and Laravel 11 2025-08-18 09:15:08 +00:00
Timothée Jaussoin
4d0c713174 Feature/312 363 364 publish redis external account 2025-07-24 14:53:30 +00:00
Timothée Jaussoin
507e913241 Fix FLEXIAPI-362 Return an empty object and not an empty array in the... 2025-07-22 10:20:30 +02:00
Timothée Jaussoin
1a79ae7b16 Fix FLEXIAPI-360 Add rules on some jobs to only run them in the Gitlab pipeline when needed 2025-07-17 16:08:04 +02:00
Timothée Jaussoin
dcb071b5bc Fix FLEXIAPI-354 Fix contact deletion 2025-07-15 17:25:03 +02:00
Timothée Jaussoin
005072e301 Fix FLEXIAPI-355 Add withoutGlobalScope() to the Account ContactVcardList resolver 2025-07-10 15:20:10 +00:00
Timothée Jaussoin
da7b401a67 Fix FLEXIAPI-356 Cleanup and reorganize the pipeline to mutualize some things and save time 2025-07-10 16:45:13 +02:00
Timothée Jaussoin
9d98e466eb Fix FLEXIAPI-352 Add missing errors box in the password change form 2025-07-09 09:40:51 +02:00
Timothée Jaussoin
33373be186 Fix FLEXIAPI-351 Fix import of CSV generated on Windows 2025-07-08 16:32:19 +02:00
Timothée Jaussoin
41e754b424 Fix FLEXIAPI-350 Fix wrongly assigned variables in some views 2025-07-08 16:15:49 +02:00
Timothée Jaussoin
327c017b8f Fix FLEXIAPI-348 Add a fallback 404 page for URLs that are pointing to no configured Spaces 2025-07-03 16:05:28 +02:00
Timothée Jaussoin
c6170f5f07 Fix FLEXIAPI-346 Complete the supporting text for the provisioning ini field 2025-07-03 15:14:04 +02:00
Timothée Jaussoin
a8a90e197b Fix FLEXIAPI-342 Enforce password change when the External Account domain is changed 2025-07-03 10:21:05 +02:00
Timothée Jaussoin
dd1345d1ba Fix FLEXIAPI-341 Allow realm to be empty when creating a Space 2025-07-02 10:34:59 +02:00
Jonathan Bartet
a1f73095fd Fix FLEXIAPI-326 Rework email templates and translations 2025-07-01 12:15:33 +02:00
Timothée Jaussoin
e50aeefbfa Fix FLEXIAPI-340 Fix the space resolution when getting the realm on Accounts 2025-06-30 16:40:25 +02:00
Timothée Jaussoin
cb5afe3343 Fix FLEXIAPI-337 Generate the provisioning URLs based on the user space 2025-06-24 15:09:16 +02:00
Timothée Jaussoin
5be36f5bbd Fix FLEXIAPI-333 Remove HTML buttons because they cannot be rendered in "old" Outlook versions 2025-06-23 16:17:32 +02:00
Timothée Jaussoin
1bc0ae9233 Fix FLEXIAPI-336 Fix broken ph icons 2025-06-19 11:15:48 +02:00
Timothée Jaussoin
22797eb493 Fix FLEXIAPI-335 Safari rendering issues with font icons 2025-06-19 10:13:53 +02:00
Timothée Jaussoin
e4392951c7 Fix FLEXIAPI-324 Add an app setup wizard page 2025-06-17 16:56:50 +02:00
Timothée Jaussoin
532bb3e096 Fix FLEXIAPI-330 Remove the ConfirmedRegistration email and related code 2025-06-17 11:27:02 +02:00
Timothée Jaussoin
ab0755b2e9 Fix FLEXIAPI-329 Use correct routes for accounts devices 2025-06-16 17:26:40 +02:00
Timothée Jaussoin
8f4da0dc7b Fix FLEXIAPI-332 Check if the first line was untouched and that the number of... 2025-06-16 14:33:41 +00:00
Timothée Jaussoin
724e4c4e5b Fix/325 328 2025-06-11 15:36:51 +00:00
Timothée Jaussoin
b590995b4e Fix/318 319 321 322 2025-06-09 12:33:05 +00:00
Timothée Jaussoin
9b58c3b0b1 Fix FLEXIAPI-322 Api Keys documentation 2025-06-05 11:45:48 +02:00
Timothée Jaussoin
e6d2b8ee9a Fix FLEXIAPI-313 Fix the admin device deletion link, recover the missing... 2025-06-05 09:15:02 +00:00
Timothée Jaussoin
f130380809 Fix FLEXIAPI-320 Domain filtering in statistics 2025-06-04 16:14:48 +02:00
Timothée Jaussoin
ad8e520047 Fix FLEXIAPI-317 Add missing form field for account_realm 2025-06-04 15:42:10 +02:00
Timothée Jaussoin
d8635a619c Fix FLEXIAPI-316 Typos in the views 2025-06-04 15:15:18 +02:00
Félix Olart
c3dcbab9cc fix: FLEXIAPI-311 Typos 2025-06-02 12:35:01 +02:00
208 changed files with 5368 additions and 2951 deletions

View file

@ -14,6 +14,14 @@ rocky9-deploy:
- rocky9-package
- rocky9-test
rocky10-deploy:
extends: .deploy
script:
- ./deploy_packages.sh rockylinux 10
needs:
- rocky10-package
- rocky10-test
debian12-deploy:
extends: .deploy
script:
@ -22,26 +30,24 @@ debian12-deploy:
- debian12-package
- debian12-test
debian13-deploy:
extends: .deploy
script:
- ./deploy_packages.sh debian trixie
needs:
- debian13-package
- debian13-test
remi-rocky8-deploy:
extends: .deploy
rules:
- changes:
- .gitlab-ci.yml
#rules:
#- changes:
# - .gitlab-ci.yml
script:
- ./deploy_packages.sh rockylinux 8
needs:
- remi-rocky8-package
remi-rocky9-deploy:
extends: .deploy
rules:
- changes:
- .gitlab-ci.yml
script:
- ./deploy_packages.sh rockylinux 9
needs:
- remi-rocky9-package
.deploy:
stage: deploy
tags: ["docker"]

View file

@ -4,12 +4,6 @@ rocky8-package:
extends: .package
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
script:
# We install this dependency only for the pipeline
- dnf -y install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
- dnf -y module reset php
- dnf -y module enable php:remi-8.2
- dnf -y update php\*
- dnf -y install php-sodium
- make package-el8
rocky9-package:
@ -18,20 +12,28 @@ rocky9-package:
extends: .package
image: gitlab.linphone.org:4567/bc/public/docker/rocky9-php:$ROCKY_9_IMAGE_VERSION
script:
# We install this dependency only for the pipeline
- dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
- dnf -y module reset php
- dnf -y module enable php:remi-8.2
- dnf -y update php\*
- dnf -y install php-sodium
- make package-el9
rocky10-package:
needs:
- prepare-package
extends: .package
image: gitlab.linphone.org:4567/bc/public/docker/rocky10-php:$ROCKY_10_IMAGE_VERSION
script:
- make package-el10
debian12-package:
needs:
- prepare-package
extends: .debian_package
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION
debian13-package:
needs:
- prepare-package
extends: .debian_package
image: gitlab.linphone.org:4567/bc/public/docker/debian13-php:$DEBIAN_13_IMAGE_VERSION
.debian_package:
extends: .package
script:
@ -46,21 +48,12 @@ remi-rocky8-package:
- dnf -y module reset redis
- dnf -y install @redis:6
remi-rocky9-package:
image: gitlab.linphone.org:4567/bc/public/docker/rocky9-php:$ROCKY_9_IMAGE_VERSION
extends: .remi-rocky-package
variables:
ROCKY_RELEASE: 9
before_script:
- dnf -y module reset redis
- dnf -y install @redis:7
.remi-rocky-package:
extends: .package
rules:
- if: $CI_COMMIT_REF_NAME =~ /^release/ || $CI_COMMIT_REF_NAME == "master"
- changes:
- .gitlab-ci.yml
#- changes:
# - .gitlab-ci.yml
script:
# Remi
- mkdir -p $CI_PROJECT_DIR/build

View file

@ -10,12 +10,18 @@ rocky9-test:
needs:
- rocky9-package
rocky10-test:
extends: .rocky-test
image: gitlab.linphone.org:4567/bc/public/docker/rocky10-php:$ROCKY_10_IMAGE_VERSION
needs:
- rocky10-package
.rocky-test:
extends: .test
script:
- yum -y localinstall build/*.rpm
- cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi
- composer install --ignore-platform-req=ext-sodium # Rocky 8 use the external library
- composer install --ignore-platform-req=ext-sodium # Rocky 8 and 9 use the external library
- vendor/bin/phpcs
- vendor/bin/phpmd . ansi phpmd.xml
- php artisan key:generate
@ -27,10 +33,16 @@ debian12-test:
needs:
- debian12-package
debian13-test:
extends: .debian-test
image: gitlab.linphone.org:4567/bc/public/docker/debian13-php:$DEBIAN_13_IMAGE_VERSION
needs:
- debian13-package
.debian-test:
extends: .test
script:
- apt update
#- apt update
- apt install -y ./build/*.deb
- cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi
- composer install

View file

@ -1,7 +1,9 @@
variables:
ROCKY_8_IMAGE_VERSION: 20250702_171834_update_rocky8_dockerhub
ROCKY_9_IMAGE_VERSION: 20250702_171314_update_rocky9_dockerhub
DEBIAN_12_IMAGE_VERSION: 20241204_162237_update_download_linphone_org
ROCKY_10_IMAGE_VERSION: 20250908_164454_rocky10_first
DEBIAN_12_IMAGE_VERSION: 20250908_154742_refresh_dependencies
DEBIAN_13_IMAGE_VERSION: 20251204_115628_update_packages
PHP_REDIS_REMI_VERSION: php-pecl-redis6-6.1.0-1
PHP_IGBINARY_REMI_VERSION: php-pecl-igbinary-3.2.16-2
PHP_MSGPACK_REMI_VERSION: php-pecl-msgpack-2.2.0-3

View file

@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [2.1]
### Added
- **Add CardDav servers** They can be configured in the administration panels and the API.
- **Rockylinux 10 support** Packages are now available in the official repository
- **Artisan cleanup script for statistics** Add an artisan console script to clear statistics after n days `app:clear-statistics {days} {--apply}`
- **Add Voicemail features and related API endpoints** to integrate with `flexisip-voicemail`
### Changed
- **Contacts Lists** The Contacts Lists are now handled per Space. During the migration, if there is only one Space present, existing Contacts Lists are automatically attached to it, otherwise the first Super Space available is used. If they are then attached to the wrong Space you'll have to change directly their `space_id` value in the `contacts_lists` database table.
- **PHP 8.2 minimum** Laravel and its dependencies were upgraded to version 11 as well.
- **Logout the user when the password is correctly changed**
## [2.0]
### Added

View file

@ -24,6 +24,14 @@ FlexiAPI is packaged for Debian and RedHat, you can setup those repositories usi
The `artisan` script is in the root directory of where the application is setup, with packages its often `/opt/belledonne-communications/share/flexisip-account-manager/flexiapi/`.
⚠️ If you want to enable JWT authentication the php-sodium dependency is required, on Rockylinux it is only available in the Remi repository in some cases. You can install it with the following steps:
dnf -y install https://rpms.remirepo.net/enterprise/remi-release-{rockylinux-release}.rpm
dnf -y module reset php
dnf -y module enable php:remi-{php-version}
dnf -y update php\*
dnf -y install php-sodium
# 2. Web server configuration
The package will deploy a `flexisip-account-manager.conf` file in the apache2 configuration directory.
@ -31,6 +39,8 @@ This file can be loaded and configured in your specific VirtualHost configuratio
To know more about the web server configuration part, you can directly [visit the official Laravel installation documentation](https://laravel.com/docs/).
⚠️ The Account Manager is handling files upload, please ensure that you raised `upload_max_filesize` and `post_max_size` to a reasonable number in your `php.ini` file to prevent file upload errors.
# 3. .env file configuration
Complete all the variables in the `.env` file (from the `.env.example` one if you setup the instance manually) or by overwriting them in your Docker or web-server configuration.

View file

@ -50,7 +50,7 @@ package-common:
cp -R cron/ $(OUTPUT_DIR)/flexisip-account-manager/
cp flexisip-account-manager.spec.run $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
tar cvf flexisip-account-manager.tar.gz -C $(OUTPUT_DIR) flexisip-account-manager
tar cf flexisip-account-manager.tar.gz -C $(OUTPUT_DIR) flexisip-account-manager
mv flexisip-account-manager.tar.gz $(OUTPUT_DIR)/rpmbuild/SOURCES/flexisip-account-manager.tar.gz
package-end-common:
@ -59,15 +59,20 @@ package-end-common:
rpm-el8-only:
mkdir -p build
sed -i 's/Requires:.*/Requires: php >= 8.1, php-gd, php-pdo, php-redis, php-mysqlnd, php-mbstring/g' $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild -v -bb --define 'dist .el8' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
sed -i 's/Requires:.*/Requires: php >= 8.2, php-gd, php-pdo, php-redis, php-mysqlnd, php-mbstring/g' $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild --quiet -bb --define 'dist .el8' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
@echo "📦✅ RPM el8 Package Created"
rpm-el9-only:
mkdir -p build
rpmbuild -v -bb --define 'dist .el9' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild --quiet -bb --define 'dist .el9' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
@echo "📦✅ RPM el9 Package Created"
rpm-el10-only:
mkdir -p build
rpmbuild --quiet -bb --define 'dist .el10' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
@echo "📦✅ RPM el10 Package Created"
rpm-cleanup:
@echo "🧹 Cleanup"
mv rpmbuild/*/*.rpm build/.
@ -76,11 +81,11 @@ rpm-cleanup:
deb-only:
mkdir -p build
sed -i 's/posttrans/post/g' $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild -v -bb --with deb --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmfilename tmp.rpm" --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild --quiet -bb --with deb --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmfilename tmp.rpm" --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
fakeroot alien -g -k --scripts $(OUTPUT_DIR)/rpmbuild/tmp.rpm
rm -r $(OUTPUT_DIR)/rpmbuild
rm -rf $(OUTPUT_DIR)/*.orig
sed -i 's/Depends:.*/Depends: $${shlibs:Depends}, php (>= 8.1), php-xml, php-pdo, php-gd, php-redis, php-mysql, php-mbstring, php-sqlite3/g' $(OUTPUT_DIR)/bc-flexisip-account-manager*/debian/control
sed -i 's/Depends:.*/Depends: $${shlibs:Depends}, php (>= 8.2), php-xml, php-pdo, php-gd, php-redis, php-mysql, php-mbstring, php-sqlite3/g' $(OUTPUT_DIR)/bc-flexisip-account-manager*/debian/control
cd `ls -rt $(OUTPUT_DIR) | tail -1` && dpkg-buildpackage --no-sign
@echo "📦✅ DEB Package Created"
@ -101,6 +106,10 @@ package-el9: rpm-el9-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el9: prepare-common package-el9
rpm-el9-dev: prepare-dev package-semvers package-common package-el9
package-el10: rpm-el10-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el10: prepare-common package-el10
rpm-el10-dev: prepare-dev package-semvers package-common package-el10
package-deb: deb-only cleanup-package-semvers package-end-common
deb: prepare-common package-deb
deb-dev: prepare-dev package-semvers package-common package-deb

1
cron/flexiapi.cron Normal file
View file

@ -0,0 +1 @@
* * * * * apache /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/artisan schedule:run >> /dev/null 2>&1

View file

@ -1,9 +1,10 @@
#!/bin/sh
cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/
sudo -su www-data && php artisan digest:clear-nonces 60
sudo -su www-data && php artisan accounts:clear-api-keys 60
sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply
sudo -su www-data && php artisan accounts:clear-api-keys 60
sudo -su www-data && php artisan accounts:clear-files 30 --apply
sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply
sudo -su www-data && php artisan spaces:expiration-emails
sudo -su www-data && php artisan app:clear-statistics 30 --apply
sudo -su www-data && php artisan digest:clear-nonces 60
sudo -su www-data && php artisan spaces:expiration-emails

View file

@ -1,9 +1,10 @@
#!/bin/sh
cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/
php artisan digest:clear-nonces 60
php artisan accounts:clear-api-keys 60
php artisan accounts:clear-accounts-tombstones 7 --apply
php artisan accounts:clear-api-keys 60
php artisan accounts:clear-files 30 --apply
php artisan accounts:clear-unconfirmed 30 --apply
php artisan app:clear-statistics 30 --apply
php artisan digest:clear-nonces 60
php artisan spaces:expiration-emails
php artisan app:clear-statistics 30 --apply

View file

@ -97,3 +97,5 @@ HCAPTCHA_SITEKEY=site-key
JWT_RSA_PUBLIC_KEY_PEM=
JWT_SIP_IDENTIFIER=
# Temporary toggles
APP_SHOW_LOGIN_COUNTER_TEMP= # default true

View file

@ -23,8 +23,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Collection;
use Carbon\Carbon;
use Awobaz\Compoships\Compoships;
@ -36,7 +36,7 @@ class Account extends Authenticatable
use HasFactory;
use Compoships;
protected $with = ['passwords', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries'];
protected $with = ['passwords', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries', 'carddavServers'];
protected $hidden = ['expire_time', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries'];
protected $appends = ['realm', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary'];
protected $casts = [
@ -73,6 +73,9 @@ class Account extends Authenticatable
return;
}
/**
* config('app.sip_domain') is required for the Tests suit
*/
$builder->where('domain', config('app.sip_domain') ?? space()->domain);
});
}
@ -131,6 +134,23 @@ class Account extends Authenticatable
return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id');
}
public function files()
{
return $this->hasMany(AccountFile::class)->latest();
}
public function voicemails()
{
return $this->hasMany(AccountFile::class)
->whereIn('content_type', AccountFile::VOICEMAIL_CONTENTTYPES)
->latest();
}
public function uploadedVoicemails()
{
return $this->voicemails()->whereNotNull('name');
}
public function vcardsStorage()
{
return $this->hasMany(VcardStorage::class);
@ -146,6 +166,12 @@ class Account extends Authenticatable
return $this->hasMany(AccountDictionaryEntry::class);
}
public function carddavServers()
{
return $this->belongsToMany(SpaceCardDavServer::class, 'account_carddav_credentials', 'account_id', 'space_carddav_server_id')
->withPivot('username', 'realm', 'algorithm', 'password');
}
public function getDictionaryAttribute()
{
if ($this->dictionaryEntries->isEmpty()) return new stdClass;
@ -318,6 +344,15 @@ class Account extends Authenticatable
return null;
}
public function getRemainingCardDavCredentialsCreatableAttribute(): Collection
{
return $this->space->carddavServers()->whereNotIn('id', function ($query) {
$query->select('space_carddav_server_id')
->from('account_carddav_credentials')
->where('account_id', $this->id);
})->get();
}
public function getIdentifierAttribute(): string
{
return $this->attributes['username'] . '@' . $this->attributes['domain'];

View file

@ -0,0 +1,20 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AccountCardDavCredentials extends Model
{
protected $table = 'account_carddav_credentials';
public function cardDavServer()
{
return $this->hasOne(SpaceCardDavServer::class, 'id', 'space_carddav_server_id');
}
public function getIdentifierAttribute()
{
return $this->username . '@' . $this->domain;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Support\Facades\Storage;
class AccountFile extends Model
{
use HasUuids;
public const VOICEMAIL_CONTENTTYPES = ['audio/opus', 'audio/wav'];
public const FILES_PATH = 'files';
protected $hidden = ['account_id', 'updated_at', 'sending_by_mail_at', 'sent_by_mail_at', 'sending_by_mail_tryouts'];
protected $appends = ['download_url'];
protected $casts = [
'uploaded_at' => 'datetime',
];
protected static function booted()
{
static::deleting(function (AccountFile $accountFile) {
Storage::delete($accountFile->getPathAttribute());
});
}
public function account()
{
return $this->belongsTo(Account::class)->withoutGlobalScopes();
}
public function getMaxUploadSizeAttribute(): ?int
{
return maxUploadSize();
}
public function getUploadUrlAttribute(): ?string
{
return route('file.upload', $this->attributes['id']);
}
public function getPathAttribute(): string
{
return self::FILES_PATH . '/' . $this->attributes['name'];
}
public function getUrlAttribute(): ?string
{
return !empty($this->attributes['name'])
&& !empty($this->attributes['id'])
? replaceHost(
route('file.show', ['uuid' => $this->attributes['id'], 'name' => $this->attributes['name']]),
$this->account->space->host
)
: null;
}
public function getDownloadUrlAttribute(): ?string
{
return !empty($this->attributes['name'])
&& !empty($this->attributes['id'])
? replaceHost(route(
'file.download',
['uuid' => $this->attributes['id'], 'name' => $this->attributes['name']]
), $this->account->space->host)
: null;
}
public function isVoicemailAudio(): bool
{
return in_array($this->attributes['content_type'], self::VOICEMAIL_CONTENTTYPES);
}
}

View file

@ -29,11 +29,6 @@ class ClearAccountsTombstones extends Command
protected $signature = 'accounts:clear-accounts-tombstones {days} {--apply}';
protected $description = 'Clear deleted accounts tombstones after n days';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$tombstones = AccountTombstone::where(

View file

@ -0,0 +1,35 @@
<?php
namespace App\Console\Commands\Accounts;
use App\AccountFile;
use Carbon\Carbon;
use Illuminate\Console\Command;
class ClearFiles extends Command
{
protected $signature = 'accounts:clear-files {days} {--apply}';
protected $description = 'Remove the uploaded files after n days';
public function handle(): int
{
$files = AccountFile::where(
'created_at',
'<',
Carbon::now()->subDays($this->argument('days'))->toDateTimeString()
);
$count = $files->count();
if ($this->option('apply')) {
$this->info($count . ' files in deletion…');
$files->delete();
$this->info($count . ' files deleted');
return Command::SUCCESS;
}
$this->info($count . ' files to delete');
return Command::SUCCESS;
}
}

View file

@ -29,11 +29,6 @@ class ClearUnconfirmed extends Command
protected $signature = 'accounts:clear-unconfirmed {days} {--apply} {--and-confirmed}';
protected $description = 'Clear unconfirmed accounts after n days';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$accounts = Account::where(

View file

@ -30,18 +30,15 @@ class CreateAdminTest extends Command
protected $signature = 'accounts:create-admin-test';
protected $description = 'Create a test admin account, only for tests purpose';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$username = 'admin_test';
$domain = 'sip.example.org';
$this->call('spaces:create-update', [
'domain' => $domain,
'sip_domain' => $domain,
'host' => $domain,
'name' => $domain,
'--super' => 'true'
]);

View file

@ -28,11 +28,6 @@ class Seed extends Command
protected $signature = 'accounts:seed {json-file-path}';
protected $description = 'Seed some accounts from a JSON file';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$file = $this->argument('json-file-path');

View file

@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands\Accounts;
use App\AccountFile;
use App\Mail\Voicemail;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendVoicemailsEmails extends Command
{
protected $signature = 'accounts:send-voicemails-emails {--tryout}';
protected $description = 'Send the voicemail emails';
public function handle()
{
$voicemails = AccountFile::whereNotNull('uploaded_at')
->whereNull('sent_by_mail_at')
->where('sending_by_mail_tryouts', '<', is_int($this->option('tryout'))
? $this->option('tryout')
: 3)
->get();
foreach ($voicemails as $voicemail) {
$voicemail->sending_by_mail_at = Carbon::now();
$voicemail->save();
if (Mail::to(users: $voicemail->account)->send(new Voicemail($voicemail))) {
$voicemail->sent_by_mail_at = Carbon::now();
$this->info('Voicemail sent to ' . $voicemail->account->identifier);
} else {
$voicemail->sending_by_mail_tryouts++;
$this->info('Error sending voicemail to ' . $voicemail->account->identifier);
}
$voicemail->save();
}
}
}

View file

@ -28,11 +28,6 @@ class SetAdmin extends Command
protected $signature = 'accounts:set-admin {id}';
protected $description = 'Give the admin role to an account';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$account = Account::withoutGlobalScopes()->where('id', $this->argument('id'))->first();

View file

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\PhoneCountry;
use Illuminate\Console\Command;
use libphonenumber\PhoneNumberUtil;
class UpdatePhoneCountries extends Command
{
protected $signature = 'app:update-phone-countries';
protected $description = 'Update the phone_countries table from the getCountryCodes() function';
public function handle()
{
$phoneNumberUtils = PhoneNumberUtil::getInstance();
$countryCodes = getCountryCodes();
foreach (array_diff(
array_keys($countryCodes),
PhoneCountry::pluck('code')->toArray()
) as $code) {
if ($resolvedMetadata = $phoneNumberUtils->getMetadataForRegion($code)) {
$phoneCountry = new PhoneCountry();
$phoneCountry->code = $code;
$phoneCountry->country_code = $resolvedMetadata->getCountryCode();
$phoneCountry->save();
$this->info($code . ' - ' . $countryCodes[$code] . ' inserted');
}
}
}
}

View file

@ -15,4 +15,9 @@ class ContactsList extends Model
{
return $this->belongsToMany(Account::class, 'contacts_list_contact', 'contacts_list_id', 'contact_id');
}
public function space()
{
return $this->belongsTo(Space::class);
}
}

View file

@ -17,6 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use App\Account;
@ -28,24 +29,15 @@ use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use Illuminate\Support\Facades\DB;
$hostSpace = null;
function space($reload = false): ?Space
function space(): ?Space
{
global $hostSpace;
if ($hostSpace != null && $reload == false) {
return $hostSpace;
}
$hostSpace = Space::where('host', request()->host())->first();
return $hostSpace;
return is_object(request()->space) ? request()->space : null;
}
function passwordAlgorithms(): array
{
return [
'MD5' => 'md5',
'MD5' => 'md5',
'SHA-256' => 'sha256',
];
}
@ -109,7 +101,7 @@ function markdownDocumentationView(string $view): string
$converter->getEnvironment()->addExtension(new TableOfContentsExtension());
return (string) $converter->convert(
(string)view($view, [
(string) view($view, [
'app_name' => space()->name
])->render()
);
@ -167,7 +159,15 @@ function resolveDomain(Request $request): string
&& $request->user()
&& $request->user()->superAdmin
? $request->get('domain')
: config('app.sip_domain');
: $request->space->domain;
}
function maxUploadSize(): int
{
return min(
ini_parse_quantity(ini_get('upload_max_filesize')),
ini_parse_quantity(ini_get('post_max_size'))
);
}
function captchaConfigured(): bool
@ -214,7 +214,7 @@ function validateIsoDate($attribute, $value, $parameters, $validator): bool
// Regex from https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
: '/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/';
return (bool)preg_match($regex, $value);
return (bool) preg_match($regex, $value);
}
/**
@ -226,236 +226,253 @@ function validateIsoDate($attribute, $value, $parameters, $validator): bool
function getCountryCodes()
{
return [
'AF' => 'Afghanistan',
'AX' => 'Åland Islands',
'AL' => 'Albania',
'DZ' => 'Algeria',
'AS' => 'American Samoa',
'AD' => 'Andorra',
'AO' => 'Angola',
'AI' => 'Anguilla',
'AE' => 'United Arab Emirates',
'AF' => 'Afghanistan',
'AG' => 'Antigua & Barbuda',
'AI' => 'Anguilla',
'AL' => 'Albania',
'AM' => 'Armenia',
'AO' => 'Angola',
'AQ' => 'Antarctica',
'AR' => 'Argentina',
'AU' => 'Australia',
'AS' => 'American Samoa',
'AT' => 'Austria',
'AU' => 'Australia',
'AW' => 'Aruba',
'AX' => 'Åland Islands',
'AZ' => 'Azerbaijan',
'BS' => 'Bahamas',
'BH' => 'Bahrain',
'BD' => 'Bangladesh',
'BB' => 'Barbados',
'BY' => 'Belarus',
'BE' => 'Belgium',
'BZ' => 'Belize',
'BJ' => 'Benin',
'BM' => 'Bermuda',
'BT' => 'Bhutan',
'BO' => 'Bolivia',
'BA' => 'Bosnia & Herzegovina',
'BW' => 'Botswana',
'BR' => 'Brazil',
'IO' => 'British Indian Ocean Territory',
'BN' => 'Brunei',
'BG' => 'Bulgaria',
'BB' => 'Barbados',
'BD' => 'Bangladesh',
'BE' => 'Belgium',
'BF' => 'Burkina Faso',
'BG' => 'Bulgaria',
'BH' => 'Bahrain',
'BI' => 'Burundi',
'KH' => 'Cambodia',
'CM' => 'Cameroon',
'BJ' => 'Benin',
'BL' => 'St. Barthélemy',
'BM' => 'Bermuda',
'BN' => 'Brunei',
'BO' => 'Bolivia',
'BQ' => 'Bonaire, Sint Eustatius & Saba',
'BR' => 'Brazil',
'BS' => 'Bahamas',
'BT' => 'Bhutan',
'BV' => 'Bouvet Island',
'BW' => 'Botswana',
'BY' => 'Belarus',
'BZ' => 'Belize',
'CA' => 'Canada',
'CV' => 'Cape Verde',
'KY' => 'Cayman Islands',
'CF' => 'Central African Republic',
'TD' => 'Chad',
'CL' => 'Chile',
'CN' => 'China',
'CX' => 'Christmas Island',
'CC' => 'Cocos (Keeling) Islands',
'CO' => 'Colombia',
'KM' => 'Comoros',
'CG' => 'Congo - Brazzaville',
'CD' => 'Congo - Kinshasa',
'CF' => 'Central African Republic',
'CG' => 'Congo - Brazzaville',
'CH' => 'Switzerland',
'CI' => "Côte d'Ivoire",
'CK' => 'Cook Islands',
'CL' => 'Chile',
'CM' => 'Cameroon',
'CN' => 'China',
'CO' => 'Colombia',
'CR' => 'Costa Rica',
'CI' => 'Côte dIvoire',
'HR' => 'Croatia',
'CU' => 'Cuba',
'CV' => 'Cabo Verde',
'CW' => 'Curaçao',
'CX' => 'Christmas Island',
'CY' => 'Cyprus',
'CZ' => 'Czechia',
'DK' => 'Denmark',
'DE' => 'Germany',
'DJ' => 'Djibouti',
'DK' => 'Denmark',
'DM' => 'Dominica',
'DO' => 'Dominican Republic',
'DZ' => 'Algeria',
'EC' => 'Ecuador',
'EG' => 'Egypt',
'SV' => 'El Salvador',
'GQ' => 'Equatorial Guinea',
'ER' => 'Eritrea',
'EE' => 'Estonia',
'EG' => 'Egypt',
'EH' => 'Western Sahara',
'ER' => 'Eritrea',
'ES' => 'Spain',
'ET' => 'Ethiopia',
'FK' => 'Falkland Islands',
'FO' => 'Faroe Islands',
'FJ' => 'Fiji',
'FI' => 'Finland',
'FJ' => 'Fiji',
'FK' => 'Falkland Islands',
'FM' => 'Micronesia',
'FO' => 'Faroe Islands',
'FR' => 'France',
'GF' => 'French Guiana',
'PF' => 'French Polynesia',
'GA' => 'Gabon',
'GM' => 'Gambia',
'GB' => 'United Kingdom',
'GD' => 'Grenada',
'GE' => 'Georgia',
'DE' => 'Germany',
'GF' => 'French Guiana',
'GG' => 'Guernsey',
'GH' => 'Ghana',
'GI' => 'Gibraltar',
'GR' => 'Greece',
'GL' => 'Greenland',
'GD' => 'Grenada',
'GP' => 'Guadeloupe',
'GU' => 'Guam',
'GT' => 'Guatemala',
'GG' => 'Guernsey',
'GM' => 'Gambia',
'GN' => 'Guinea',
'GP' => 'Guadeloupe',
'GQ' => 'Equatorial Guinea',
'GR' => 'Greece',
'GS' => 'South Georgia & South Sandwich Islands',
'GT' => 'Guatemala',
'GU' => 'Guam',
'GW' => 'Guinea-Bissau',
'GY' => 'Guyana',
'HT' => 'Haiti',
'HK' => 'Hong Kong',
'HM' => 'Heard & McDonald Islands',
'HN' => 'Honduras',
'HK' => 'Hong Kong SAR China',
'HR' => 'Croatia',
'HT' => 'Haiti',
'HU' => 'Hungary',
'IS' => 'Iceland',
'IN' => 'India',
'ID' => 'Indonesia',
'IR' => 'Iran',
'IQ' => 'Iraq',
'IE' => 'Ireland',
'IM' => 'Isle of Man',
'IL' => 'Israel',
'IM' => 'Isle of Man',
'IN' => 'India',
'IO' => 'British Indian Ocean Territory',
'IQ' => 'Iraq',
'IR' => 'Iran',
'IS' => 'Iceland',
'IT' => 'Italy',
'JM' => 'Jamaica',
'JP' => 'Japan',
'JE' => 'Jersey',
'JM' => 'Jamaica',
'JO' => 'Jordan',
'KZ' => 'Kazakhstan',
'JP' => 'Japan',
'KE' => 'Kenya',
'KG' => 'Kyrgyzstan',
'KH' => 'Cambodia',
'KI' => 'Kiribati',
'KM' => 'Comoros',
'KN' => 'St. Kitts & Nevis',
'KP' => 'North Korea',
'KR' => 'South Korea',
'KW' => 'Kuwait',
'KG' => 'Kyrgyzstan',
'KY' => 'Cayman Islands',
'KZ' => 'Kazakhstan',
'LA' => 'Laos',
'LV' => 'Latvia',
'LB' => 'Lebanon',
'LS' => 'Lesotho',
'LR' => 'Liberia',
'LY' => 'Libya',
'LC' => 'St. Lucia',
'LI' => 'Liechtenstein',
'LK' => 'Sri Lanka',
'LR' => 'Liberia',
'LS' => 'Lesotho',
'LT' => 'Lithuania',
'LU' => 'Luxembourg',
'MO' => 'Macao SAR China',
'MK' => 'North Macedonia',
'LV' => 'Latvia',
'LY' => 'Libya',
'MA' => 'Morocco',
'MC' => 'Monaco',
'MD' => 'Moldova',
'ME' => 'Montenegro',
'MF' => 'St. Martin',
'MG' => 'Madagascar',
'MW' => 'Malawi',
'MY' => 'Malaysia',
'MV' => 'Maldives',
'ML' => 'Mali',
'MT' => 'Malta',
'MH' => 'Marshall Islands',
'MK' => 'North Macedonia',
'ML' => 'Mali',
'MM' => 'Myanmar',
'MN' => 'Mongolia',
'MO' => 'Macao',
'MP' => 'Northern Mariana Islands',
'MQ' => 'Martinique',
'MR' => 'Mauritania',
'MU' => 'Mauritius',
'YT' => 'Mayotte',
'MX' => 'Mexico',
'FM' => 'Micronesia',
'MD' => 'Moldova',
'MC' => 'Monaco',
'MN' => 'Mongolia',
'ME' => 'Montenegro',
'MS' => 'Montserrat',
'MA' => 'Morocco',
'MT' => 'Malta',
'MU' => 'Mauritius',
'MV' => 'Maldives',
'MW' => 'Malawi',
'MX' => 'Mexico',
'MY' => 'Malaysia',
'MZ' => 'Mozambique',
'MM' => 'Myanmar (Burma)',
'NA' => 'Namibia',
'NR' => 'Nauru',
'NP' => 'Nepal',
'NL' => 'Netherlands',
'NC' => 'New Caledonia',
'NZ' => 'New Zealand',
'NI' => 'Nicaragua',
'NE' => 'Niger',
'NG' => 'Nigeria',
'NU' => 'Niue',
'NF' => 'Norfolk Island',
'MP' => 'Northern Mariana Islands',
'NG' => 'Nigeria',
'NI' => 'Nicaragua',
'NL' => 'Netherlands',
'NO' => 'Norway',
'NP' => 'Nepal',
'NR' => 'Nauru',
'NU' => 'Niue',
'NZ' => 'New Zealand',
'OM' => 'Oman',
'PK' => 'Pakistan',
'PW' => 'Palau',
'PS' => 'Palestinian Territories',
'PA' => 'Panama',
'PG' => 'Papua New Guinea',
'PY' => 'Paraguay',
'PE' => 'Peru',
'PF' => 'French Polynesia',
'PG' => 'Papua New Guinea',
'PH' => 'Philippines',
'PK' => 'Pakistan',
'PL' => 'Poland',
'PT' => 'Portugal',
'PM' => 'St. Pierre & Miquelon',
'PN' => 'Pitcairn Islands',
'PR' => 'Puerto Rico',
'PS' => 'Palestine',
'PT' => 'Portugal',
'PW' => 'Palau',
'PY' => 'Paraguay',
'QA' => 'Qatar',
'RE' => 'Réunion',
'RO' => 'Romania',
'RS' => 'Serbia',
'RU' => 'Russia',
'RW' => 'Rwanda',
'SH' => 'St. Helena',
'KN' => 'St. Kitts & Nevis',
'LC' => 'St. Lucia',
'PM' => 'St. Pierre & Miquelon',
'VC' => 'St. Vincent & Grenadines',
'WS' => 'Samoa',
'SM' => 'San Marino',
'ST' => 'São Tomé & Príncipe',
'SA' => 'Saudi Arabia',
'SN' => 'Senegal',
'RS' => 'Serbia',
'SC' => 'Seychelles',
'SL' => 'Sierra Leone',
'SG' => 'Singapore',
'SK' => 'Slovakia',
'SI' => 'Slovenia',
'SB' => 'Solomon Islands',
'SO' => 'Somalia',
'ZA' => 'South Africa',
'ES' => 'Spain',
'LK' => 'Sri Lanka',
'SC' => 'Seychelles',
'SD' => 'Sudan',
'SR' => 'Suriname',
'SJ' => 'Svalbard & Jan Mayen',
'SZ' => 'Eswatini',
'SE' => 'Sweden',
'CH' => 'Switzerland',
'SG' => 'Singapore',
'SH' => 'St. Helena',
'SI' => 'Slovenia',
'SJ' => 'Svalbard & Jan Mayen',
'SK' => 'Slovakia',
'SL' => 'Sierra Leone',
'SM' => 'San Marino',
'SN' => 'Senegal',
'SO' => 'Somalia',
'SR' => 'Suriname',
'SS' => 'South Sudan',
'ST' => 'São Tomé & Príncipe',
'SV' => 'El Salvador',
'SX' => 'Sint Maarten',
'SY' => 'Syria',
'TW' => 'Taiwan',
'TJ' => 'Tajikistan',
'TZ' => 'Tanzania',
'TH' => 'Thailand',
'TL' => 'Timor-Leste',
'TG' => 'Togo',
'TK' => 'Tokelau',
'TO' => 'Tonga',
'TT' => 'Trinidad & Tobago',
'TN' => 'Tunisia',
'TM' => 'Turkmenistan',
'SZ' => 'Eswatini',
'TC' => 'Turks & Caicos Islands',
'TD' => 'Chad',
'TF' => 'French Southern Territories',
'TG' => 'Togo',
'TH' => 'Thailand',
'TJ' => 'Tajikistan',
'TK' => 'Tokelau',
'TL' => 'Timor-Leste',
'TM' => 'Turkmenistan',
'TN' => 'Tunisia',
'TO' => 'Tonga',
'TR' => 'Türkiye',
'TT' => 'Trinidad & Tobago',
'TV' => 'Tuvalu',
'UG' => 'Uganda',
'TW' => 'Taiwan',
'TZ' => 'Tanzania',
'UA' => 'Ukraine',
'AE' => 'United Arab Emirates',
'GB' => 'United Kingdom',
'UG' => 'Uganda',
'UM' => 'U.S. Minor Outlying Islands',
'US' => 'United States',
'UY' => 'Uruguay',
'UZ' => 'Uzbekistan',
'VU' => 'Vanuatu',
'VA' => 'Holy See (Vatican City)',
'VC' => 'St. Vincent & Grenadines',
'VE' => 'Venezuela',
'VN' => 'Vietnam',
'VG' => 'British Virgin Islands',
'VI' => 'U.S. Virgin Islands',
'VN' => 'Vietnam',
'VU' => 'Vanuatu',
'WF' => 'Wallis & Futuna',
'EH' => 'Western Sahara',
'WS' => 'Samoa',
'YE' => 'Yemen',
'YT' => 'Mayotte',
'ZA' => 'South Africa',
'ZM' => 'Zambia',
'ZW' => 'Zimbabwe',
];

View file

@ -43,9 +43,9 @@ class AuthenticateController extends Controller
return redirect()->route('account.dashboard');
}
return view('account.login', [
return view('account.login', config('app.show_login_counter_temp') ? [
'count' => Account::where('activated', true)->count()
]);
]: []);
}
public function authenticate(Request $request)

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Account;
use App\AccountFile;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
class FileController extends Controller
{
public function show(string $uuid, string $name)
{
$file = AccountFile::findOrFail($uuid);
if ($file->name != $name) {
abort(404);
}
return Storage::get($file->path);
}
public function download(string $uuid, string $name)
{
$file = AccountFile::findOrFail($uuid);
if ($file->name != $name) {
abort(404);
}
return Storage::download($file->path);
}
}

View file

@ -21,6 +21,7 @@ namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PasswordController extends Controller
@ -46,12 +47,10 @@ class PasswordController extends Controller
if ($account->passwords()->count() > 0) {
Log::channel('events')->info('Web: Password changed', ['id' => $account->identifier]);
return redirect()->route('account.dashboard');
return redirect()->route('account.logout');
}
Log::channel('events')->info('Web: Password set for the first time', ['id' => $account->identifier]);
return redirect()->route('account.dashboard');
return redirect()->route('account.logout');
}
}

View file

@ -151,7 +151,7 @@ class ProvisioningController extends Controller
private function checkProvisioningHeader(Request $request)
{
if (!$request->hasHeader('x-linphone-provisioning')
&& space()?->provisioning_use_linphone_provisioning_header) {
&& $request->space->provisioning_use_linphone_provisioning_header) {
abort(400, 'x-linphone-provisioning header is missing');
}
}
@ -172,8 +172,8 @@ class ProvisioningController extends Controller
$dom->appendChild($config);
if (space()?->custom_provisioning_entries) {
$rc = parse_ini_string(space()->custom_provisioning_entries, true);
if ($request->space?->custom_provisioning_entries) {
$rc = parse_ini_string($request->space->custom_provisioning_entries, true);
foreach ($rc as $sectionName => $values) {
$section = $dom->createElement('section');
@ -189,6 +189,44 @@ class ProvisioningController extends Controller
}
}
$remoteContactDirectoryCounter = 0;
$authInfoIndex = 0;
// CardDav servers
if ($request->space?->carddavServers) {
foreach ($request->space->carddavServers as $carddavServer) {
$carddavServer->getProvisioningSection($config, $remoteContactDirectoryCounter);
$remoteContactDirectoryCounter++;
}
}
if ($account) {
foreach ($account->carddavServers as $carddavServer) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$config->appendChild($section);
$entry = $dom->createElement('entry', $carddavServer->pivot->username);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->realm);
$entry->setAttribute('name', 'realm');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->password);
$entry->setAttribute('name', 'ha1');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
$authInfoIndex++;
}
}
// Password reset
if ($account && $request->has('reset_password')) {
$account->updatePassword(Str::random(10));
@ -251,7 +289,6 @@ class ProvisioningController extends Controller
}
$passwords = $account->passwords()->get();
$authInfoIndex = 0;
foreach ($passwords as $password) {
$section = $xpath->query("//section[@name='auth_info_" . $authInfoIndex . "']")->item(0);
@ -297,7 +334,7 @@ class ProvisioningController extends Controller
}
// Overwrite all the entries
if (space()?->custom_provisioning_overwrite_all) {
if ($request->space?->custom_provisioning_overwrite_all) {
$xpath = new \DOMXpath($dom);
$entries = $xpath->query("//section/entry");
if (!is_null($entries)) {

View file

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Account;
use App\ResetPasswordEmailToken;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ResetPasswordEmailController extends Controller
{

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Facades\Log;
use App\Account;
use App\AccountType;
class AccountAccountTypeController extends Controller
class AccountTypeController extends Controller
{
public function create(int $id)
{

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -27,7 +27,7 @@ use App\Account;
use App\AccountAction;
use App\Rules\NoUppercase;
class AccountActionController extends Controller
class ActionController extends Controller
{
public function create(int $accountId)
{

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use App\Account;
class AccountActivityController extends Controller
class ActivityController extends Controller
{
public function index(int $accountId)
{

View file

@ -0,0 +1,104 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\AccountCardDavCredentials;
use App\Http\Controllers\Controller;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use App\Http\Requests\Account\CardDavCredentials;
class CardDavCredentialsController extends Controller
{
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
return view('admin.account.carddav.create', [
'account' => $account,
'carddavServers' => $account->remainingCardDavCredentialsCreatable
]);
}
public function store(CardDavCredentials $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$request->validate([
'carddav_id' => ['required', Rule::exists('space_carddav_servers', 'id')->where(function (Builder $query) use ($account) {
return $query->where('space_id', $account->space->id);
})]
]);
$accountCarddavCredentials = new AccountCardDavCredentials;
$accountCarddavCredentials->space_carddav_server_id = $request->get('carddav_id');
$accountCarddavCredentials->account_id = $account->id;
$accountCarddavCredentials->username = $request->get('username');
$accountCarddavCredentials->realm = $request->get('realm');
$accountCarddavCredentials->password = bchash(
$request->get('username'),
$request->get('realm'),
$request->get('password'),
$request->get('algorithm')
);
$accountCarddavCredentials->algorithm = $request->get('algorithm');
$accountCarddavCredentials->save();
return redirect()->route('admin.account.show', $account);
}
public function delete(int $accountId, int $cardDavId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$accountCarddavCredentials = AccountCardDavCredentials::where('space_carddav_server_id', $cardDavId)
->where('account_id', $account->id)
->firstOrFail();
return view('admin.account.carddav.delete', [
'account' => $account,
'carddavCredentials' => $accountCarddavCredentials,
]);
}
public function destroy(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$accountCarddavCredentials = AccountCardDavCredentials::where('space_carddav_server_id', $request->carddav_id)
->where('account_id', $account->id)
->delete();
return redirect()->route('admin.account.show', $account);
}
private function checkFeatureEnabled(Account $account)
{
if (!$account->space->carddav_user_credentials) {
abort(403, 'CardDav Credentials features disabled');
}
}
}

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Facades\Log;
use App\Account;
use App\ContactsList;
class AccountContactController extends Controller
class ContactController extends Controller
{
public function index(int $accountId)
{

View file

@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Libraries\FlexisipRedisConnector;
class AccountDeviceController extends Controller
class DeviceController extends Controller
{
public function index(int $accountId)
{

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -25,7 +25,7 @@ use Illuminate\Http\Request;
use App\Account;
use App\AccountDictionaryEntry;
class AccountDictionaryController extends Controller
class DictionaryController extends Controller
{
public function create(int $accountId)
{

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class FileController extends Controller
{
public function delete(int $accountId, string $fileId)
{
$account = Account::findOrFail($accountId);
$file = $account->files()->where('id', $fileId)->firstOrFail();
return view('admin.account.file.delete', [
'account' => $account,
'file' => $file
]);
}
public function destroy(Request $request, int $accountId, string $fileId)
{
$account = Account::findOrFail($accountId);
$accountFile = $account->files()
->where('id', $fileId)
->firstOrFail();
$accountFile->delete();
return redirect()->route('admin.account.show', $account)->withFragment('#files');
}
}

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\ExternalAccount;
@ -32,7 +32,7 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
use Propaganistas\LaravelPhone\PhoneNumber;
class AccountImportController extends Controller
class ImportController extends Controller
{
private Collection $errors;
private string $importDirectory = 'imported_csv';
@ -307,7 +307,7 @@ class AccountImportController extends Controller
'account_id' => $passwordAccount->id,
'password' => bchash(
$passwordAccount->username,
space()?->account_realm ?? $domain,
$request->space?->account_realm ?? $domain,
$passwords[$passwordAccount->username],
$algorithm
),

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\Http\Controllers\Controller;
@ -26,7 +26,7 @@ use App\StatisticsCall;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AccountStatisticsController extends Controller
class StatisticsController extends Controller
{
public function edit(Request $request, int $accountId)
{

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -27,7 +27,7 @@ use Illuminate\Validation\Rule;
use App\AccountType;
use App\Rules\NoUppercase;
class AccountTypeController extends Controller
class TypeController extends Controller
{
public function index()
{

View file

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\CardDavServer;
use Illuminate\Http\Request;
use App\Space;
use App\SpaceCardDavServer;
class CardDavServerController extends Controller
{
public function create(Space $space)
{
return view('admin.space.carddav_server.create_edit', [
'space' => $space,
'carddavServer' => new SpaceCardDavServer
]);
}
public function store(CardDavServer $request, Space $space)
{
$carddavServer = new SpaceCardDavServer;
$carddavServer->space_id = $space->id;
$carddavServer->fill($request->validated());
$carddavServer->enabled = getRequestBoolean($request, 'enabled');
$carddavServer->use_exact_match_policy = getRequestBoolean($request, 'use_exact_match_policy');
$carddavServer->save();
return redirect()->route('admin.spaces.integration', $space);
}
public function edit(Space $space, int $carddavServerId)
{
return view('admin.space.carddav_server.create_edit', [
'space' => $space,
'carddavServer' => $space->carddavServers()->findOrFail($carddavServerId)
]);
}
public function update(CardDavServer $request, Space $space, int $carddavServerId)
{
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->fill($request->validated());
$carddavServer->enabled = getRequestBoolean($request, 'enabled');
$carddavServer->use_exact_match_policy = getRequestBoolean($request, 'use_exact_match_policy');
$carddavServer->save();
return redirect()->route('admin.spaces.integration', $space);
}
public function delete(Space $space, int $carddavServerId)
{
return view('admin.space.carddav_server.delete', [
'space' => $space,
'carddavServer' => $space->carddavServers()->findOrFail($carddavServerId)
]);
}
public function destroy(Space $space, int $carddavServerId)
{
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->delete();
return redirect()->route('admin.spaces.integration', $space->id);
}
}

View file

@ -17,18 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Space;
use App\Account;
use App\ContactsList;
use App\Space;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ContactsListContactController extends Controller
{
public function add(Request $request, int $contactsListId)
public function add(Request $request, Space $space, int $contactsListId)
{
$accounts = Account::orderBy('updated_at', $request->get('updated_at_order', 'desc'));
$accounts = $space->accounts()->orderBy('updated_at', $request->get('updated_at_order', 'desc'));
if ($request->has('search')) {
$accounts = $accounts->where('username', 'like', '%' . $request->get('search') . '%');
@ -38,9 +39,9 @@ class ContactsListContactController extends Controller
$accounts = $accounts->where('domain', $request->get('domain'));
}
return view('admin.contacts_list.contacts.add', [
'domains' => Account::groupBy('domain')->pluck('domain'),
'contacts_list' => ContactsList::findOrFail($contactsListId),
return view('admin.space.contacts_list.contacts.add', [
'space' => $space,
'contacts_list' => $space->contactsLists()->findOrFail($contactsListId),
'params' => [
'contacts_list_id' => $contactsListId
],
@ -52,33 +53,33 @@ class ContactsListContactController extends Controller
]);
}
public function search(Request $request, int $contactsListId)
public function search(Request $request, Space $space, int $contactsListId)
{
return redirect()->route('admin.contacts_lists.contacts.add', ['contacts_list_id' => $contactsListId] + $request->except('_token'));
return redirect()->route('admin.spaces.contacts_lists.contacts.add', ['contacts_list_id' => $contactsListId] + $request->except('_token'));
}
public function store(Request $request, int $contactsListId)
public function store(Request $request, Space $space, int $contactsListId)
{
$request->validate([
'contacts_ids' => 'required|exists:accounts,id'
]);
$contactsList = ContactsList::findOrFail($contactsListId);
$contactsList = $space->contactsLists()->findOrFail($contactsListId);
$contactsList->contacts()->detach($request->get('contacts_ids')); // Just in case
$contactsList->contacts()->attach($request->get('contacts_ids'));
return redirect()->route('admin.contacts_lists.edit', $contactsList->id);
return redirect()->route('admin.spaces.contacts_lists.edit', [$space, $contactsList->id]);
}
public function destroy(Request $request, int $contactsListId)
public function destroy(Request $request, Space $space, int $contactsListId)
{
$request->validate([
'contacts_ids' => 'required|exists:accounts,id'
]);
$contactsList = ContactsList::findOrFail($contactsListId);
$contactsList = $space->contactsLists()->findOrFail($contactsListId);
$contactsList->contacts()->detach($request->get('contacts_ids'));
return redirect()->route('admin.contacts_lists.edit', $contactsList->id);
return redirect()->route('admin.spaces.contacts_lists.edit', [$space, $contactsList->id]);
}
}

View file

@ -17,61 +17,67 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\Admin\Space;
use App\Account;
use App\ContactsList;
use App\Space;
use App\Http\Controllers\Controller;
use Illuminate\Validation\Rule;
use Illuminate\Http\Request;
class ContactsListController extends Controller
{
public function index(Request $request)
public function index(Request $request, Space $space)
{
$request->validate([
'order_by' => 'in:title,updated_at,contacts_count',
'order_sort' => 'in:asc,desc',
]);
$contactsLists = ContactsList::orderBy($request->get('order_by', 'updated_at'), $request->get('order_sort', 'desc'));
$contactsLists = $space->contactsLists()->orderBy($request->get('order_by', 'updated_at'), $request->get('order_sort', 'desc'));
return view('admin.contacts_list.index', [
return view('admin.space.contacts_list.index', [
'space' => $space,
'contacts_lists' => $contactsLists
->paginate(20)
->appends($request->query()),
]);
}
public function create(Request $request)
public function create(Request $request, Space $space)
{
return view('admin.contacts_list.create_edit', [
return view('admin.space.contacts_list.create_edit', [
'space' => $space,
'contacts_list' => new ContactsList,
]);
}
public function store(Request $request)
public function store(Request $request, Space $space)
{
$request->validate([
'title' => 'required|unique:contacts_lists'
]);
$contactsList = new ContactsList;
$contactsList->space_id = $space->id;
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
return redirect()->route('admin.contacts_lists.edit', $contactsList->id);
return redirect()->route('admin.spaces.contacts_lists.edit', [$space, $contactsList->id]);
}
public function search(Request $request, int $contactsListId)
public function search(Request $request, Space $space, int $contactsListId)
{
return redirect()->route('admin.contacts_lists.edit', ['contacts_list_id' => $contactsListId] + $request->except('_token'));
return redirect()->route('admin.spaces.contacts_lists.edit', [
'space' => $space,
'contacts_list_id' => $contactsListId] + $request->except('_token'));
}
public function edit(Request $request, int $id)
public function edit(Request $request, Space $space, int $id)
{
$contacts = ContactsList::findOrFail($id)->contacts();
$contacts = $space->contactsLists()->findOrFail($id)->contacts();
if ($request->has('search')) {
$contacts = $contacts->where('username', 'like', '%' . $request->get('search') . '%');
@ -83,14 +89,15 @@ class ContactsListController extends Controller
$contacts = $contacts->get();
return view('admin.contacts_list.create_edit', [
return view('admin.space.contacts_list.create_edit', [
'space' => $space,
'domains' => Account::groupBy('domain')->pluck('domain'),
'contacts_list' => ContactsList::findOrFail($id),
'contacts_list' => $space->contactsLists()->findOrFail($id),
'contacts' => $contacts
]);
}
public function update(Request $request, int $id)
public function update(Request $request, Space $space, int $id)
{
$request->validate([
'title' => [
@ -99,26 +106,27 @@ class ContactsListController extends Controller
],
]);
$contactsList = ContactsList::findOrFail($id);
$contactsList = $space->contactsLists()->findOrFail($id);
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
return redirect()->route('admin.contacts_lists.index');
return redirect()->route('admin.spaces.contacts_lists.index', $space);
}
public function delete(int $id)
public function delete(Space $space, int $id)
{
return view('admin.contacts_list.delete', [
'contacts_list' => ContactsList::findOrFail($id),
return view('admin.space.contacts_list.delete', [
'space' => $space,
'contacts_list' => $space->contactsLists()->findOrFail($id),
]);
}
public function destroy(Request $request)
public function destroy(Request $request, Space $space)
{
$contactsList = ContactsList::findOrFail($request->get('contacts_lists_id'));
$contactsList = $space->contactsLists()->findOrFail($request->get('contacts_lists_id'));
$contactsList->delete();
return redirect()->route('admin.contacts_lists.index');
return redirect()->route('admin.spaces.contacts_lists.index', $space);
}
}

View file

@ -146,6 +146,7 @@ class SpaceController extends Controller
$space->expire_at = $request->get('expire_at');
$space->custom_theme = getRequestBoolean($request, 'custom_theme');
$space->web_panel = getRequestBoolean($request, 'web_panel');
$space->carddav_user_credentials = getRequestBoolean($request, 'carddav_user_credentials');
$space->save();
return redirect()->route('admin.spaces.show', $space);

View file

@ -108,7 +108,7 @@ class StatisticsController extends Controller
$domain = $request->user()->superAdmin
? $request->get('domain')
: $request->user()->domain;
: $request->space->domain;
if ($domain) {
$fromQuery->where('to_domain', $domain);

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Api\Account;
use App\AccountFile;
use App\Http\Controllers\Controller;
use App\Rules\AudioMime;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class FileController extends Controller
{
public function upload(Request $request, string $uuid)
{
$file = AccountFile::findOrFail($uuid);
if (!empty($file->name)) {
abort(404);
}
$request->validate(['file' => 'required|file']);
if ($file->isVoicemailAudio()) {
$request->validate(['file' => [new AudioMime($file)]]);
}
$uploadedFile = $request->file('file');
$name = Str::random(8) . '_' . $uploadedFile->getClientOriginalName();
if ($uploadedFile->storeAs(AccountFile::FILES_PATH, $name)) {
$file->name = $name;
$file->size = $uploadedFile->getSize();
$file->uploaded_at = Carbon::now();
$file->save();
return $file;
}
abort(503);
}
}

View file

@ -10,12 +10,12 @@ class VcardsStorageController extends Controller
{
public function index(Request $request)
{
return (new AdminVcardsStorageController)->index($request->user()->id);
return (new AdminVcardsStorageController)->index($request, $request->user()->id);
}
public function show(Request $request, string $uuid)
{
return (new AdminVcardsStorageController)->show($request->user()->id, $uuid);
return (new AdminVcardsStorageController)->show($request, $request->user()->id, $uuid);
}
public function store(Request $request)

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api\Account;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Api\Admin\Account\VoicemailController as AdminVoicemailController;
use Illuminate\Http\Request;
class VoicemailController extends Controller
{
public function index(Request $request)
{
return (new AdminVoicemailController)->index($request, $request->user()->id);
}
public function store(Request $request)
{
return (new AdminVoicemailController)->store($request, $request->user()->id);
}
public function show(Request $request, string $uuid)
{
return (new AdminVoicemailController)->show($request, $request->user()->id, $uuid);
}
public function destroy(Request $request, string $uuid)
{
return (new AdminVoicemailController)->destroy($request, $request->user()->id, $uuid);
}
}

View file

@ -17,33 +17,32 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Account;
use App\AccountAction;
use App\Rules\NoUppercase;
class AccountActionController extends Controller
class ActionController extends Controller
{
public function index(int $id)
public function index(Request $request, int $accountId)
{
return $this->resolveAccount($id)->actions;
return $this->resolveAccount($request, $accountId)->actions;
}
public function get(int $id, int $actionId)
public function get(Request $request, int $accountId, int $actionId)
{
return $this->resolveAccount($id)
return $this->resolveAccount($request, $accountId)
->actions()
->where('id', $actionId)
->firstOrFail();
}
public function store(Request $request, int $id)
public function store(Request $request, int $accountId)
{
$account = $this->resolveAccount($id);
$account = $this->resolveAccount($request, $accountId);
$request->validate([
'key' => ['required', 'alpha_dash', new NoUppercase],
@ -59,9 +58,9 @@ class AccountActionController extends Controller
return $accountAction;
}
public function update(Request $request, int $id, int $actionId)
public function update(Request $request, int $accountId, int $actionId)
{
$account = $this->resolveAccount($id);
$account = $this->resolveAccount($request, $accountId);
$request->validate([
'key' => ['alpha_dash', new NoUppercase],
@ -79,17 +78,17 @@ class AccountActionController extends Controller
return $accountAction;
}
public function destroy(int $id, int $actionId)
public function destroy(Request $request, int $accountId, int $actionId)
{
return $this->resolveAccount($id)
return $this->resolveAccount($request, $accountId)
->actions()
->where('id', $actionId)
->delete();
}
private function resolveAccount(int $id)
private function resolveAccount(Request $request, int $accountId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
if ($account->dtmf_protocol == null) abort(403, 'DTMF Protocol must be configured');
return $account;

View file

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Account;
use App\Space;
use App\AccountCardDavCredentials;
use App\SpaceCardDavServer;
use App\Http\Requests\Account\CardDavCredentials;
class CardDavCredentialsController extends Controller
{
public function index(Request $request, int $accountId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServers = $account->carddavServers;
if ($cardDavServers->isEmpty()) return new \stdClass;
return $cardDavServers->map(function ($cardDavServer) {
return $this->extractCardDavServer($cardDavServer);
})->keyBy('carddav_id');
}
public function show(Request $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $account->cardDavServers()->findOrFail($cardDavServerId);
return $this->extractCardDavServer($cardDavServer);
}
public function update(CardDavCredentials $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $request->space->cardDavServers()->findOrFail($cardDavServerId);
$accountCarddavCredentials = AccountCardDavCredentials::where('account_id', $account->id)
->where('space_carddav_server_id', $cardDavServer->id)
->delete();
$accountCarddavCredentials = new AccountCardDavCredentials;
$accountCarddavCredentials->space_carddav_server_id = $cardDavServer->id;
$accountCarddavCredentials->account_id = $account->id;
$accountCarddavCredentials->username = $request->get('username');
$accountCarddavCredentials->realm = $request->get('realm');
$accountCarddavCredentials->password = bchash(
$request->get('username'),
$request->get('realm'),
$request->get('password'),
$request->get('algorithm')
);
$accountCarddavCredentials->algorithm = $request->get('algorithm');
return $accountCarddavCredentials->save();
}
public function destroy(Request $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $account->cardDavServers()->findOrFail($cardDavServerId);
return $cardDavServer->delete();
}
private function extractCardDavServer(SpaceCardDavServer $cardDavServer)
{
return [
'carddav_id' => $cardDavServer->id,
'username' => $cardDavServer->pivot->username,
'realm' => $cardDavServer->pivot->realm,
'algorithm' => $cardDavServer->pivot->algorithm,
'password' => $cardDavServer->pivot->password,
];
}
}

View file

@ -17,40 +17,39 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Account;
class AccountContactController extends Controller
class ContactController extends Controller
{
public function index(int $id)
public function index(Request $request, int $accountId)
{
return Account::findOrFail($id)->contacts;
return $request->space->accounts()->findOrFail($accountId)->contacts;
}
public function show(int $id, int $contactId)
public function show(Request $request, int $accountId, int $contactId)
{
return Account::findOrFail($id)
return $request->space->accounts()->findOrFail($accountId)
->contacts()
->where('id', $contactId)
->firstOrFail();
}
public function add(int $id, int $contactId)
public function add(Request $request, int $accountId, int $contactId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
$account->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
if ($request->space->accounts()->findOrFail($contactId)) {
return $account->contacts()->attach($contactId);
}
}
public function remove(int $id, int $contactId)
public function remove(Request $request, int $accountId, int $contactId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->contacts()->pluck('id')->contains($contactId)) {
abort(404);

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Str;
use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class AccountCreationTokenController extends Controller
class CreationTokenController extends Controller
{
public function create(Request $request)
{

View file

@ -17,23 +17,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use App\Account;
use Illuminate\Http\Request;
class AccountDictionaryController extends Controller
class DictionaryController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
return Account::findOrFail($accountId)->dictionary;
return $request->space->accounts()->findOrFail($accountId)->dictionary;
}
public function show(int $accountId, string $key)
public function show(Request $request, int $accountId, string $key)
{
return Account::findOrFail($accountId)->dictionaryEntries()->where('key', $key)->first();
return $request->space->accounts()
->findOrFail($accountId)->dictionaryEntries()->where('key', $key)->first();
}
public function set(Request $request, int $accountId, string $key)
@ -42,7 +42,7 @@ class AccountDictionaryController extends Controller
'value' => 'required'
]);
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$result = $account->setDictionaryEntry($key, $request->get('value'));
if (function_exists('accountServiceAccountEditedHook')) {
@ -53,8 +53,15 @@ class AccountDictionaryController extends Controller
return $result;
}
public function destroy(int $accountId, string $key)
public function destroy(Request $request, int $accountId, string $key)
{
return Account::findOrFail($accountId)->dictionaryEntries()->where('key', $key)->delete();
return $request->space->accounts()
->findOrFail($accountId)->dictionaryEntries()->where('key', $key)->delete();
}
public function clear(Request $request, int $accountId)
{
return $request->space->accounts()
->findOrFail($accountId)->dictionaryEntries()->delete();
}
}

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use App\Rules\NoUppercase;
@ -25,7 +25,7 @@ use Illuminate\Http\Request;
use App\AccountType;
class AccountTypeController extends Controller
class TypeController extends Controller
{
public function index()
{

View file

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api\Admin\Account;
use App\Account;
use App\AccountFile;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class VoicemailController extends Controller
{
public function index(Request $request, int $accountId)
{
return Account::findOrFail($accountId)->voicemails;
}
public function store(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$request->validate([
'sip_from' => 'nullable|starts_with:sip',
'content_type' => [
'required',
Rule::in(AccountFile::VOICEMAIL_CONTENTTYPES),
]
]);
$voicemail = new AccountFile;
$voicemail->account_id = $account->id;
$voicemail->sip_from = $request->get('sip_from');
$voicemail->content_type = $request->get('content_type');
$voicemail->save();
$voicemail->append(['upload_url', 'max_upload_size']);
return $voicemail;
}
public function show(Request $request, int $accountId, string $uuid)
{
return Account::findOrFail($accountId)->voicemails()->where('id', $uuid)->firstOrFail();
}
public function destroy(Request $request, int $accountId, string $uuid)
{
return Account::findOrFail($accountId)->voicemails()->where('id', $uuid)->delete();
}
}

View file

@ -25,7 +25,6 @@ use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Account;
use App\AccountTombstone;
use App\AccountType;
use App\ContactsList;
@ -35,18 +34,23 @@ use App\Http\Requests\Account\Update\Api\AsAdminRequest as ApiAsAdminRequest;
use App\Mail\Provisioning;
use App\Mail\ResetPassword;
use App\Services\AccountService;
use App\Space;
class AccountController extends Controller
{
public function index(Request $request)
{
return Account::without(['passwords', 'admin'])->with(['phoneChangeCode', 'emailChangeCode'])->paginate(20);
return $request->space->accounts()
->without(['passwords', 'admin'])
->with(['phoneChangeCode', 'emailChangeCode'])
->paginate(20);
}
public function show(Request $request, $accountId)
{
$account = Account::without(['passwords', 'admin'])->with(['phoneChangeCode', 'emailChangeCode'])->findOrFail($accountId);
$account = $request->space->accounts()
->without(['passwords', 'admin'])
->with(['phoneChangeCode', 'emailChangeCode'])
->findOrFail($accountId);
if ($request->user()->admin) {
if ($account->phoneChangeCode) {
@ -61,27 +65,29 @@ class AccountController extends Controller
return $account;
}
public function search(string $sip)
public function search(Request $request, string $sip)
{
$account = Account::sip($sip)->first();
$account = $request->space->accounts()->sip($sip)->first();
if (!$account) abort(404, 'SIP address not found');
if (!$account)
abort(404, 'SIP address not found');
return $account;
}
public function searchByEmail(string $email)
public function searchByEmail(Request $request, string $email)
{
$account = Account::where('email', $email)->first();
$account = $request->space->accounts()->where('email', $email)->first();
if (!$account) abort(404, 'Email address not found');
if (!$account)
abort(404, 'Email address not found');
return $account;
}
public function destroy(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->hasTombstone()) {
$tombstone = new AccountTombstone();
@ -95,9 +101,9 @@ class AccountController extends Controller
Log::channel('events')->info('API Admin: Account destroyed', ['id' => $account->identifier]);
}
public function activate(int $accountId)
public function activate(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->activated = true;
$account->save();
@ -106,9 +112,9 @@ class AccountController extends Controller
return $account;
}
public function deactivate(int $accountId)
public function deactivate(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->activated = false;
$account->save();
@ -117,9 +123,9 @@ class AccountController extends Controller
return $account;
}
public function block(int $accountId)
public function block(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->blocked = true;
$account->save();
@ -128,9 +134,9 @@ class AccountController extends Controller
return $account;
}
public function unblock(int $accountId)
public function unblock(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->blocked = false;
$account->save();
@ -139,9 +145,9 @@ class AccountController extends Controller
return $account;
}
public function provision(int $accountId)
public function provision(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->provision();
$account->save();
@ -164,55 +170,56 @@ class AccountController extends Controller
return $account->makeVisible(['provisioning_token']);
}
public function typeAdd(int $accountId, int $typeId)
public function typeAdd(Request $request, int $accountId, int $typeId)
{
if (Account::findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
if ($request->space->accounts()->findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
abort(403);
}
if (AccountType::findOrFail($typeId)) {
return Account::findOrFail($accountId)->types()->attach($typeId);
return $request->space->accounts()->findOrFail($accountId)->types()->attach($typeId);
}
}
public function typeRemove(int $accountId, int $typeId)
public function typeRemove(Request $request, int $accountId, int $typeId)
{
if (!Account::findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
if (!$request->space->accounts()->findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
abort(403);
}
return Account::findOrFail($accountId)->types()->detach($typeId);
return $request->space->accounts()->findOrFail($accountId)->types()->detach($typeId);
}
public function contactsListAdd(int $accountId, int $contactsListId)
public function contactsListAdd(Request $request, int $accountId, int $contactsListId)
{
if (Account::findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
if ($request->space->accounts()->findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
if (ContactsList::findOrFail($contactsListId)) {
return Account::findOrFail($accountId)->contactsLists()->attach($contactsListId);
return $request->space->accounts()->findOrFail($accountId)->contactsLists()->attach($contactsListId);
}
}
public function contactsListRemove(int $accountId, int $contactsListId)
public function contactsListRemove(Request $request, int $accountId, int $contactsListId)
{
if (!Account::findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
if (!$request->space->accounts()->findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
return Account::findOrFail($accountId)->contactsLists()->detach($contactsListId);
return $request->space->accounts()->findOrFail($accountId)->contactsLists()->detach($contactsListId);
}
/**
* Emails
*/
public function sendProvisioningEmail(int $accountId)
public function sendProvisioningEmail(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
if (!$account->email)
abort(403, 'No email configured');
$account->provision();
@ -221,11 +228,12 @@ class AccountController extends Controller
Log::channel('events')->info('API: Sending provisioning email', ['id' => $account->identifier]);
}
public function sendResetPasswordEmail(int $accountId)
public function sendResetPasswordEmail(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
if (!$account->email)
abort(403, 'No email configured');
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;

View file

@ -22,21 +22,27 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Libraries\FlexisipRedisConnector;
use App\Account;
use Illuminate\Http\Request;
use stdClass;
class DeviceController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
$devices = (new FlexisipRedisConnector)->getDevices(Account::findOrFail($accountId)->identifier);
$devices = (new FlexisipRedisConnector)->getDevices(
$request->space->accounts()->findOrFail($accountId)->identifier
);
return ($devices->isEmpty()) ? new stdClass : $devices;
}
public function destroy(int $accountId, string $uuid)
public function destroy(Request $request, int $accountId, string $uuid)
{
$connector = new FlexisipRedisConnector;
return $connector->deleteDevice(Account::findOrFail($accountId)->identifier, $uuid);
return $connector->deleteDevice(
$request->space->accounts()->findOrFail($accountId)->identifier,
$uuid
);
}
}

View file

@ -23,16 +23,12 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ExternalAccount\CreateUpdate;
use App\Services\AccountService;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use App\ExternalAccount;
use App\Account;
class ExternalAccountController extends Controller
{
public function show(int $accountId)
public function show(Request $request, int $accountId)
{
return Account::findOrFail($accountId)->external()->firstOrFail();
return $request->space->accounts()->findOrFail($accountId)->external()->firstOrFail();
}
public function store(CreateUpdate $request, int $accountId)

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\PhoneCountry;
class PhoneCountryController extends Controller
{
public function activate(string $code)
{
$phoneCountry = PhoneCountry::where('code', $code)->firstOrFail();
return PhoneCountry::where('country_code', $phoneCountry->country_code)->update(['activated' => true]);
}
public function deactivate(string $code)
{
$phoneCountry = PhoneCountry::where('code', $code)->firstOrFail();
return PhoneCountry::where('country_code', $phoneCountry->country_code)->update(['activated' => false]);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\CardDavServer;
use Illuminate\Http\Request;
use App\Space;
use App\SpaceCardDavServer;
class CardDavServerController extends Controller
{
public function index(string $domain)
{
return Space::where('domain', $domain)->firstOrFail()->carddavServers;
}
public function show(string $domain, int $carddavServerId)
{
return Space::where('domain', $domain)->firstOrFail()->carddavServers()->findOrFail($carddavServerId);
}
public function store(CardDavServer $request, string $domain)
{
$space = Space::where('domain', $domain)->firstOrFail();
$carddavServer = new SpaceCardDavServer;
$carddavServer->space_id = $space->id;
$carddavServer->fill($request->validated());
$carddavServer->enabled = $request->has('enabled') && (bool)$request->get('enabled');
$carddavServer->use_exact_match_policy = $request->has('use_exact_match_policy') && (bool)$request->get('use_exact_match_policy');
return $carddavServer->save();
}
public function update(CardDavServer $request, string $domain, int $carddavServerId)
{
$space = Space::where('domain', $domain)->firstOrFail();
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->fill($request->validated());
$carddavServer->enabled = $request->has('enabled') && (bool)$request->get('enabled');
$carddavServer->use_exact_match_policy = $request->has('use_exact_match_policy') && (bool)$request->get('use_exact_match_policy');
return $carddavServer->save();
}
public function destroy(string $domain, int $carddavServerId)
{
$space = Space::where('domain', $domain)->firstOrFail();
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
return $carddavServer->delete();
}
}

View file

@ -17,9 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Space;
use App\Account;
use App\ContactsList;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -28,12 +27,12 @@ class ContactsListController extends Controller
{
public function index(Request $request)
{
return ContactsList::all();
return $request->space->contactsLists;
}
public function get(int $contactsListId)
public function get(Request $request, int $contactsListId)
{
return ContactsList::findOrFail($contactsListId);
return $request->space->contactsLists()->findOrFail($contactsListId);
}
public function store(Request $request)
@ -44,6 +43,7 @@ class ContactsListController extends Controller
]);
$contactsList = new ContactsList;
$contactsList->space_id = $request->space->id;
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
@ -59,6 +59,7 @@ class ContactsListController extends Controller
]);
$contactsList = ContactsList::findOrFail($contactsListId);
$contactsList->space_id = $request->space->id;
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
@ -66,25 +67,25 @@ class ContactsListController extends Controller
return $contactsList;
}
public function destroy(int $contactsListId)
public function destroy(Request $request, int $contactsListId)
{
return ContactsList::where('id', $contactsListId)
return $request->space->contactsLists()->where('id', $contactsListId)
->delete();
}
public function contactAdd(int $id, int $contactId)
public function contactAdd(Request $request, int $id, int $contactId)
{
$contactsList = ContactsList::findOrFail($id);
$contactsList = $request->space->contactsLists()->findOrFail($id);
$contactsList->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
if ($request->space->accounts()->findOrFail($contactId)) {
return $contactsList->contacts()->attach($contactId);
}
}
public function contactRemove(int $id, int $contactId)
public function contactRemove(Request $request, int $id, int $contactId)
{
$contactsList = ContactsList::findOrFail($id);
$contactsList = $request->space->contactsLists()->findOrFail($id);
if (!$contactsList->contacts()->pluck('id')->contains($contactId)) {
abort(404);

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Api\Admin;
namespace App\Http\Controllers\Api\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\EmailServer\CreateUpdate;

View file

@ -45,38 +45,37 @@ class SpaceController extends Controller
]);
$space = new Space;
$space->name = $request->get('name');
$space->domain = $request->get('domain');
$space->host = $request->get('host');
$this->setRequestBoolean($request, $space, 'super');
$this->setRequestBoolean($request, $space, 'disable_chat_feature');
$this->setRequestBoolean($request, $space, 'disable_meetings_feature');
$this->setRequestBoolean($request, $space, 'disable_broadcast_feature');
$this->setRequestBoolean($request, $space, 'hide_settings');
$this->setRequestBoolean($request, $space, 'hide_account_settings');
$this->setRequestBoolean($request, $space, 'disable_call_recordings_feature');
$this->setRequestBoolean($request, $space, 'only_display_sip_uri_username');
$this->setRequestBoolean($request, $space, 'assistant_hide_create_account');
$this->setRequestBoolean($request, $space, 'assistant_disable_qr_code');
$this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->expire_at = $request->get('expire_at');
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');
$space->copyright_text = $request->get('copyright_text');
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
$space->domain = $request->get('domain');
$space->expire_at = $request->get('expire_at');
$space->host = $request->get('host');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->name = $request->get('name');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$this->setRequestBoolean($request, $space, 'assistant_disable_qr_code');
$this->setRequestBoolean($request, $space, 'assistant_hide_create_account');
$this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account');
$this->setRequestBoolean($request, $space, 'carddav_user_credentials');
$this->setRequestBoolean($request, $space, 'custom_provisioning_overwrite_all');
$this->setRequestBoolean($request, $space, 'provisioning_use_linphone_provisioning_header');
$this->setRequestBoolean($request, $space, 'custom_theme');
$this->setRequestBoolean($request, $space, 'web_panel');
$this->setRequestBoolean($request, $space, 'public_registration');
$this->setRequestBoolean($request, $space, 'phone_registration');
$this->setRequestBoolean($request, $space, 'disable_broadcast_feature');
$this->setRequestBoolean($request, $space, 'disable_call_recordings_feature');
$this->setRequestBoolean($request, $space, 'disable_chat_feature');
$this->setRequestBoolean($request, $space, 'disable_meetings_feature');
$this->setRequestBoolean($request, $space, 'hide_account_settings');
$this->setRequestBoolean($request, $space, 'hide_settings');
$this->setRequestBoolean($request, $space, 'intercom_features');
$this->setRequestBoolean($request, $space, 'only_display_sip_uri_username');
$this->setRequestBoolean($request, $space, 'phone_registration');
$this->setRequestBoolean($request, $space, 'provisioning_use_linphone_provisioning_header');
$this->setRequestBoolean($request, $space, 'public_registration');
$this->setRequestBoolean($request, $space, 'super');
$this->setRequestBoolean($request, $space, 'web_panel');
$space->save();
return $space->refresh();
@ -90,30 +89,30 @@ class SpaceController extends Controller
public function update(Request $request, string $domain)
{
$request->validate([
'super' => 'required|boolean',
'disable_chat_feature' => 'required|boolean',
'disable_meetings_feature' => 'required|boolean',
'disable_broadcast_feature' => 'required|boolean',
'hide_settings' => 'required|boolean',
'hide_account_settings' => 'required|boolean',
'disable_call_recordings_feature' => 'required|boolean',
'only_display_sip_uri_username' => 'required|boolean',
'assistant_hide_create_account' => 'required|boolean',
'assistant_disable_qr_code' => 'required|boolean',
'assistant_hide_third_party_account' => 'required|boolean',
'max_account' => 'required|integer',
'max_accounts' => 'required|integer',
'expire_at' => 'nullable|date|after_or_equal:today',
'account_realm' => ['nullable', new Domain()],
'assistant_disable_qr_code' => 'required|boolean',
'assistant_hide_create_account' => 'required|boolean',
'assistant_hide_third_party_account' => 'required|boolean',
'carddav_user_credentials' => 'required|boolean',
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
'custom_provisioning_overwrite_all' => 'required|boolean',
'provisioning_use_linphone_provisioning_header' => 'required|boolean',
'custom_theme' => 'required|boolean',
'web_panel' => 'required|boolean',
'public_registration' => 'required|boolean',
'phone_registration' => 'required|boolean',
'disable_broadcast_feature' => 'required|boolean',
'disable_call_recordings_feature' => 'required|boolean',
'disable_chat_feature' => 'required|boolean',
'disable_meetings_feature' => 'required|boolean',
'expire_at' => 'nullable|date|after_or_equal:today',
'hide_account_settings' => 'required|boolean',
'hide_settings' => 'required|boolean',
'intercom_features' => 'required|boolean',
'max_account' => 'required|integer',
'max_accounts' => 'required|integer',
'only_display_sip_uri_username' => 'required|boolean',
'phone_registration' => 'required|boolean',
'provisioning_use_linphone_provisioning_header' => 'required|boolean',
'public_registration' => 'required|boolean',
'super' => 'required|boolean',
'web_panel' => 'required|boolean',
]);
$space = Space::where('domain', $domain)->firstOrFail();
@ -132,33 +131,33 @@ class SpaceController extends Controller
$space->name = $request->get('name');
$space->host = $request->get('host');
$space->super = $request->get('super');
$space->disable_chat_feature = $request->get('disable_chat_feature');
$space->disable_meetings_feature = $request->get('disable_meetings_feature');
$space->disable_broadcast_feature = $request->get('disable_broadcast_feature');
$space->hide_settings = $request->get('hide_settings');
$space->hide_account_settings = $request->get('hide_account_settings');
$space->disable_call_recordings_feature = $request->get('disable_call_recordings_feature');
$space->only_display_sip_uri_username = $request->get('only_display_sip_uri_username');
$space->assistant_hide_create_account = $request->get('assistant_hide_create_account');
$space->assistant_disable_qr_code = $request->get('assistant_disable_qr_code');
$space->assistant_hide_third_party_account = $request->get('assistant_hide_third_party_account');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->expire_at = $request->get('expire_at');
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');
$space->assistant_disable_qr_code = $request->get('assistant_disable_qr_code');
$space->assistant_hide_create_account = $request->get('assistant_hide_create_account');
$space->assistant_hide_third_party_account = $request->get('assistant_hide_third_party_account');
$space->copyright_text = $request->get('copyright_text');
$space->carddav_user_credentials = $request->get('carddav_user_credentials');
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
$space->custom_provisioning_overwrite_all = $request->get('custom_provisioning_overwrite_all');
$space->provisioning_use_linphone_provisioning_header = $request->get('provisioning_use_linphone_provisioning_header');
$space->custom_theme = $request->get('custom_theme');
$space->web_panel = $request->get('web_panel');
$space->public_registration = $request->get('public_registration');
$space->phone_registration = $request->get('phone_registration');
$space->disable_broadcast_feature = $request->get('disable_broadcast_feature');
$space->disable_call_recordings_feature = $request->get('disable_call_recordings_feature');
$space->disable_chat_feature = $request->get('disable_chat_feature');
$space->disable_meetings_feature = $request->get('disable_meetings_feature');
$space->expire_at = $request->get('expire_at');
$space->hide_account_settings = $request->get('hide_account_settings');
$space->hide_settings = $request->get('hide_settings');
$space->intercom_features = $request->get('intercom_features');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->only_display_sip_uri_username = $request->get('only_display_sip_uri_username');
$space->phone_registration = $request->get('phone_registration');
$space->provisioning_use_linphone_provisioning_header = $request->get('provisioning_use_linphone_provisioning_header');
$space->public_registration = $request->get('public_registration');
$space->web_panel = $request->get('web_panel');
$space->save();

View file

@ -19,7 +19,6 @@
namespace App\Http\Controllers\Api\Admin;
use App\Account;
use App\Http\Controllers\Controller;
use App\Rules\Vcard;
use App\VcardStorage;
@ -30,15 +29,15 @@ use stdClass;
class VcardsStorageController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
$list = Account::findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
$list = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
return $list->isEmpty() ? new stdClass : $list;
}
public function show(int $accountId, string $uuid)
public function show(Request $request, int $accountId, string $uuid)
{
return Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
return $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
}
public function store(Request $request, int $accountId)
@ -48,18 +47,14 @@ class VcardsStorageController extends Controller
]);
$vcardo = VObject\Reader::read($request->get('vcard'));
$vcardoUID = substr($vcardo->UID, 9);
$request->merge(['uuid' => $vcardoUID]);
$request->validate(['uuid' => 'uuid']);
if (Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardoUID)->first()) {
if ($request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardo->UID)->first()) {
abort(409, 'Vcard already exists');
}
$vcard = new VcardStorage();
$vcard->account_id = $accountId;
$vcard->uuid = $vcardoUID;
$vcard->uuid = $vcardo->UID;
$vcard->vcard = preg_replace('/\r\n?/', "\n", $vcardo->serialize());
$vcard->save();
@ -68,24 +63,17 @@ class VcardsStorageController extends Controller
public function update(Request $request, int $accountId, string $uuid)
{
$request->merge(['uuid' => $uuid]);
$request->validate([
'uuid' => 'uuid',
'vcard' => ['required', new Vcard()]
]);
$vcardo = VObject\Reader::read($request->get('vcard'));
$vcardoUID = substr($vcardo->UID, 9);
$request->merge(['vuuid' => $vcardoUID]);
$request->validate(['vuuid' => 'uuid']);
if ($vcardoUID != $uuid) {
if ($vcardo->UID != $uuid) {
abort(422, 'UUID should be the same');
}
$vcard = Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard->vcard = preg_replace('/\r\n?/', "\n", $vcardo->serialize());
$vcard->save();
@ -94,10 +82,7 @@ class VcardsStorageController extends Controller
public function destroy(Request $request, int $accountId, string $uuid)
{
$request->merge(['uuid' => $uuid]);
$request->validate(['uuid' => 'uuid']);
$vcard = Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
return $vcard->delete();
}

View file

@ -1,116 +0,0 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\Localization::class,
'space.check',
],
'api' => [
'throttle:600,1', // move to 600 instead of 60
'bindings',
'validate_json',
'localization',
'space.check',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class,
'auth.super_admin' => \App\Http\Middleware\AuthenticateSuperAdmin::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.digest_or_key' => \App\Http\Middleware\AuthenticateDigestOrKey::class,
'auth.jwt' => \App\Http\Middleware\AuthenticateJWT::class,
'auth.check_blocked' => \App\Http\Middleware\CheckBlocked::class,
'validate_json' => \App\Http\Middleware\ValidateJSON::class,
'web_panel_enabled' => \App\Http\Middleware\IsWebPanelEnabled::class,
'public_registration' => \App\Http\Middleware\IsPublicRegistration::class,
'phone_registration' => \App\Http\Middleware\IsPhoneRegistration::class,
'intercom_features' => \App\Http\Middleware\IsIntercomFeatures::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'cookie' => \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
'cookie.encrypt' => \App\Http\Middleware\EncryptCookies::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'space.check' => \App\Http\Middleware\SpaceCheck::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'localization' => \App\Http\Middleware\Localization::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

View file

@ -29,7 +29,9 @@ class AuthenticateAdmin
return redirect()->route('account.login');
}
if (!$request->user()->admin) {
if (!$request->user()->admin
|| (!$request->user()->superAdmin && $request->user()->domain != $request->space->domain)
) {
return abort(403, 'Unauthorized area');
}

View file

@ -76,18 +76,20 @@ class AuthenticateJWT
}
$account = null;
$identifierKey = config('services.jwt.sip_identifier');
if ($identifierKey == '') $identifierKey = 'sip_identity';
if ($token->claims()->has(config('services.jwt.sip_identifier'))) {
list($username, $domain) = parseSIP($token->claims()->get(config('services.jwt.sip_identifier')));
if ($token->claims()->has($identifierKey)) {
list($username, $domain) = parseSIP($token->claims()->get($identifierKey));
$account = Account::withoutGlobalScopes()
->where('username', $username)
->where('domain', $domain)
->first();
->where('username', $username)
->where('domain', $domain)
->first();
} elseif ($token->claims()->has('email')) {
$account = Account::withoutGlobalScopes()
->where('email', $token->claims()->get('email'))
->first();
->where('email', $token->claims()->get('email'))
->first();
}
if (!$account) {
@ -99,8 +101,7 @@ class AuthenticateJWT
return $next($request);
}
if (
!empty(config('app.account_authentication_bearer'))
if (!empty(config('app.account_authentication_bearer'))
// Bypass the JWT auth if we have an API Key
&& !$request->header('x-api-key')
&& !$request->cookie('x-api-key')
@ -130,7 +131,7 @@ class AuthenticateJWT
$response = new Response();
$response->header(
'WWW-Authenticate',
$bearer . 'error="' . $error . '", error_description="'. $description . '"'
$bearer . 'error="' . $error . '", error_description="' . $description . '"'
);
$response->setStatusCode(401);

View file

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsCardDavCredentialsEnabled
{
public function handle(Request $request, Closure $next): Response
{
if ($request->space->carddav_user_credentials) {
return $next($request);
}
abort(403, 'CardDav Credentials features disabled');
}
}

View file

@ -10,7 +10,7 @@ class IsIntercomFeatures
{
public function handle(Request $request, Closure $next): Response
{
if (space()?->intercom_features) {
if ($request->space->intercom_features) {
return $next($request);
}

View file

@ -27,7 +27,7 @@ class IsPhoneRegistration
{
public function handle(Request $request, Closure $next): Response
{
if (space()?->phone_registration) {
if ($request->space->phone_registration) {
return $next($request);
}

View file

@ -27,10 +27,10 @@ class IsPublicRegistration
{
public function handle(Request $request, Closure $next): Response
{
if (space()?->public_registration) {
if ($request->space?->public_registration) {
return $next($request);
}
return abort(404, 'Public registration disabled');
abort(404, 'Public registration disabled');
}
}

View file

@ -27,7 +27,7 @@ class IsWebPanelEnabled
{
public function handle(Request $request, Closure $next)
{
if (!$request->expectsJson() && space()?->web_panel) {
if (!$request->expectsJson() && $request->space->web_panel) {
return $next($request);
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use Closure;
use App\Space;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;
@ -15,15 +16,16 @@ class SpaceCheck
abort(503, 'APP_ROOT_HOST is not configured');
}
$space = space(reload: true);
$space = Space::where('host', config('app.sip_domain') ?? request()->host())->first();
if ($space != null) {
if (!str_ends_with($space->host, config('app.root_host'))) {
abort(503, 'The APP_ROOT_HOST configured does not match with the current root domain');
}
$request->merge(['space' => $space]);
Config::set('app.url', '://' . $space->host);
Config::set('app.sip_domain', $space->domain);
if ($space->isExpired()) {
abort($request->expectsJson() ? 403 : 490, 'The related Space has expired');

View file

@ -1,18 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View file

@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -1,7 +1,7 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -17,21 +17,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Tests;
namespace App\Http\Requests\Account;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
abstract class TestCaseWithSpaceMiddleware extends BaseTestCase
use App\Rules\Domain;
use App\Rules\PasswordAlgorithm;
class CardDavCredentials extends FormRequest
{
use CreatesApplication;
use RefreshDatabase;
use TestUtilsTrait;
public function setUp(): void
public function rules()
{
parent::setUp();
config()->set('app.sip_domain', 'sip.example.com');
return [
'username' => 'required',
'password' => 'required',
'algorithm' => ['required', new PasswordAlgorithm],
'realm' => 'required',
];
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Space;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\EmailServer;
use App\Rules\CommaList;
class CardDavServer extends FormRequest
{
public function rules()
{
return [
'uri' => 'required|url',
'min_characters' => 'integer|min:1',
'results_limit' => 'integer|min:0',
'timeout' => 'integer|min:1',
'delay' => 'integer|min:100',
'fields_for_user_input' => [new CommaList],
'fields_for_domain' => [new CommaList],
];
}
}

View file

@ -58,7 +58,7 @@ class StatisticsGraphFactory
$toQuery = StatisticsMessage::query();
if (!Auth::user()?->superAdmin) {
$fromQuery->where('from_domain', space()->domain);
$fromQuery->where('from_domain', $this->request->space->domain);
$toQuery->toDomain(space()->domain);
} elseif ($this->domain) {
$fromQuery->where('from_domain', $this->domain);

View file

@ -5,7 +5,6 @@ namespace App\Mail;
use App\Space;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

View file

@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

View file

@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

View file

@ -22,7 +22,6 @@ namespace App\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

View file

@ -20,10 +20,8 @@
namespace App\Mail;
use App\Account;
use App\ResetPasswordEmailToken;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

View file

@ -0,0 +1,44 @@
<?php
namespace App\Mail;
use App\AccountFile;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Mail\Mailables\Attachment;
class Voicemail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public AccountFile $accountFile)
{
}
public function envelope(): Envelope
{
return new Envelope(
subject: $this->accountFile->account->space->name .
': ' .
__('New voice message from :sipfrom', ['sipfrom' => $this->accountFile->sip_from]),
);
}
public function content(): Content
{
return new Content(
markdown: 'mails.voicemail',
);
}
public function attachments(): array
{
return [
Attachment::fromStorage($this->accountFile->path)
->withMime($this->accountFile->content_type)
];
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Rules;
use App\AccountFile;
use Illuminate\Contracts\Validation\Rule;
class AudioMime implements Rule
{
public function __construct(private AccountFile $accountFile)
{
}
public function passes($attribute, $file): bool
{
$mimeType = null;
switch ($file->getMimeType()) {
case 'audio/opus':
$mimeType = 'audio/opus';
break;
case 'audio/vnd.wave':
case 'audio/wav':
case 'audio/wave':
case 'audio/x-wav':
case 'audio/x-pn-wav':
$mimeType = 'audio/wav';
break;
}
return $this->accountFile->content_type == $mimeType;
}
public function message()
{
return __('The file should have the declared mime-type');
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class CommaList implements Rule
{
public function passes($attribute, $value)
{
preg_match_all('/[^, ]+/', $value, $matches);
return $value == null || (!empty($matches) && (implode(',', $matches[0]) == $value));
}
public function message()
{
return 'The :attribute should be null or contain a list of words separated by commas without spaces';
}
}

View file

@ -3,7 +3,6 @@
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
use Propaganistas\LaravelPhone\PhoneNumber;
class IsNotPhoneNumber implements Rule

View file

@ -23,11 +23,6 @@ use Illuminate\Contracts\Validation\Rule;
class WithoutSpaces implements Rule
{
public function __construct()
{
//
}
public function passes($attribute, $value)
{
return preg_match('/^\S*$/u', $value);

View file

@ -62,10 +62,10 @@ class AccountService
$account = new Account();
$account->username = $request->get('username');
$account->activated = false;
$account->domain = space()->domain;
$account->domain = $request->space->domain;
$account->ip_address = $request->ip();
$account->created_at = Carbon::now();
$account->user_agent = space()->name;
$account->user_agent = $request->space->name;
$account->dtmf_protocol = $request->get('dtmf_protocol');
if ($request->asAdmin) {
@ -73,7 +73,7 @@ class AccountService
$account->display_name = $request->get('display_name');
$account->activated = $request->has('activated') ? (bool)$request->get('activated') : false;
$account->domain = resolveDomain($request);
$account->user_agent = $request->header('User-Agent') ?? space()->name;
$account->user_agent = $request->header('User-Agent') ?? $request->space->name;
$account->admin = $request->has('admin') && (bool)$request->get('admin');
if (!$request->api && $request->has('role')) {
@ -113,8 +113,8 @@ class AccountService
);
if (!$request->api) {
if (!empty(space()?->newsletter_registration_address) && $request->has('newsletter')) {
Mail::to(space()->newsletter_registration_address)->send(new NewsletterRegistration($account));
if (!empty($request->space?->newsletter_registration_address) && $request->has('newsletter')) {
Mail::to($request->space->newsletter_registration_address)->send(new NewsletterRegistration($account));
}
}

View file

@ -21,52 +21,67 @@ namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class Space extends Model
{
use HasFactory;
protected $with = ['emailServer'];
protected $with = ['emailServer', 'carddavServers'];
public const FORBIDDEN_KEYS = [
'disable_chat_feature',
'disable_meetings_feature',
'disable_broadcast_feature',
'max_account',
'hide_settings',
'hide_account_settings',
'disable_call_recordings_feature',
'only_display_sip_uri_username',
'assistant_hide_create_account',
'account_proxy_registrar_address',
'account_realm',
'assistant_disable_qr_code',
'assistant_hide_create_account',
'assistant_hide_third_party_account',
'copyright_text',
'disable_broadcast_feature',
'disable_call_recordings_feature',
'disable_chat_feature',
'disable_meetings_feature',
'hide_account_settings',
'hide_settings',
'intro_registration_text',
'max_account',
'newsletter_registration_address',
'account_proxy_registrar_address',
'account_realm'
'only_display_sip_uri_username',
];
protected $hidden = ['id'];
protected $casts = [
'super' => 'boolean',
'assistant_disable_qr_code' => 'boolean',
'assistant_hide_create_account' => 'boolean',
'assistant_hide_third_party_account' => 'boolean',
'carddav_user_credentials' => 'boolean',
'disable_broadcast_feature' => 'boolean',
'disable_call_recordings_feature' => 'boolean',
'disable_chat_feature' => 'boolean',
'disable_meetings_feature' => 'boolean',
'disable_broadcast_feature' => 'boolean',
'hide_settings' => 'boolean',
'hide_account_settings' => 'boolean',
'disable_call_recordings_feature' => 'boolean',
'only_display_sip_uri_username' => 'boolean',
'assistant_hide_create_account' => 'boolean',
'assistant_disable_qr_code' => 'boolean',
'assistant_hide_third_party_account' => 'boolean',
'expire_at' => 'date',
'hide_account_settings' => 'boolean',
'hide_settings' => 'boolean',
'only_display_sip_uri_username' => 'boolean',
'super' => 'boolean',
];
public const HOST_REGEX = '[\w\-]+';
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)';
protected static function booted()
{
static::addGlobalScope('domain', function (Builder $builder) {
if (!Auth::hasUser()) return;
if (Auth::hasUser() || Auth::user()->superAdmin) {
return;
}
$builder->where('domain', Auth::user()->domain);
});
}
public function accounts()
{
return $this->hasMany(Account::class, 'domain', 'domain');
@ -82,6 +97,16 @@ class Space extends Model
return $this->hasOne(SpaceEmailServer::class);
}
public function carddavServers()
{
return $this->hasMany(SpaceCardDavServer::class);
}
public function contactsLists()
{
return $this->hasMany(ContactsList::class);
}
public function scopeNotFull(Builder $query)
{
return $query->where('max_accounts', 0)

View file

@ -0,0 +1,82 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SpaceCardDavServer extends Model
{
protected $hidden = ['space_id'];
protected $table = 'space_carddav_servers';
protected $fillable = ['uri', 'enabled', 'min_characters', 'results_limist', 'use_exact_match_policy', 'timeout', 'delay', 'fields_for_user_input', 'fields_for_domain'];
protected $casts = [
'enabled' => 'boolean',
'use_exact_match_policy' => 'boolean',
];
public function space()
{
return $this->belongsTo(Space::class);
}
public function accounts()
{
return $this->belongsToMany(Account::class, 'account_carddav_credentials', 'space_carddav_server_id', 'account_id');
}
public function getNameAttribute()
{
return __('CardDav Server') . ' ' . $this->id;
}
public function getProvisioningSection($config, int $remoteContactDirectoryCounter)
{
$dom = $config->ownerDocument;
$section = $dom->createElement('section');
$section->setAttribute('name', 'remote_contact_directory_' . $remoteContactDirectoryCounter);
$entry = $dom->createElement('entry', $this->enabled ? '1': '0');
$entry->setAttribute('name', 'enabled');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'carddav');
$entry->setAttribute('name', 'type');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->uri);
$entry->setAttribute('name', 'uri');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->min_characters);
$entry->setAttribute('name', 'min_characters');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->results_limit);
$entry->setAttribute('name', 'results_limit');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->timeout);
$entry->setAttribute('name', 'timeout');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->delay);
$entry->setAttribute('name', 'delay');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->fields_for_user_input);
$entry->setAttribute('name', 'carddav_fields_for_user_input');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->fields_for_domain);
$entry->setAttribute('name', 'carddav_fields_for_domain');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->use_exact_match_policy ? '1': '0');
$entry->setAttribute('name', 'carddav_use_exact_match_policy');
$section->appendChild($entry);
$config->appendChild($section);
}
}

View file

@ -1,53 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$status = $app->handleCommand(new ArgvInput);
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);
exit($status);

View file

@ -1,55 +1,50 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\AuthenticateAdmin;
use App\Http\Middleware\AuthenticateDigestOrKey;
use App\Http\Middleware\AuthenticateJWT;
use App\Http\Middleware\AuthenticateSuperAdmin;
use App\Http\Middleware\CheckBlocked;
use App\Http\Middleware\IsCardDavCredentialsEnabled;
use App\Http\Middleware\IsIntercomFeatures;
use App\Http\Middleware\IsPhoneRegistration;
use App\Http\Middleware\IsPublicRegistration;
use App\Http\Middleware\IsWebPanelEnabled;
use App\Http\Middleware\Localization;
use App\Http\Middleware\SpaceCheck;
use App\Http\Middleware\ValidateJSON;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->append(SpaceCheck::class);
$middleware->append(Localization::class);
$middleware->api(append: [ValidateJSON::class]);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$middleware->alias([
'auth.admin' => AuthenticateAdmin::class,
'auth.check_blocked' => CheckBlocked::class,
'auth.digest_or_key' => AuthenticateDigestOrKey::class,
'auth.jwt' => AuthenticateJWT::class,
'auth.super_admin' => AuthenticateSuperAdmin::class,
'auth' => Authenticate::class,
'feature.carddav_user_credentials' => IsCardDavCredentialsEnabled::class,
'feature.intercom' => IsIntercomFeatures::class,
'feature.phone_registration' => IsPhoneRegistration::class,
'feature.public_registration' => IsPublicRegistration::class,
'feature.web_panel_enabled' => IsWebPanelEnabled::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

View file

@ -8,33 +8,33 @@
],
"license": "MIT",
"require": {
"php": ">=8.1",
"awobaz/compoships": "^2.3",
"doctrine/dbal": "^3.0",
"endroid/qr-code": "^5.0",
"fakerphp/faker": "^1.23",
"laravel/framework": "^10.0",
"laravel/tinker": "^2.9",
"php": ">=8.2",
"laravel/framework": "^11.45.1",
"awobaz/compoships": "^2.4.1",
"doctrine/dbal": "^3.10.1",
"endroid/qr-code": "^5.1",
"fakerphp/faker": "^1.24.1",
"laravel/tinker": "^2.10.1",
"lcobucci/jwt": "^4.3",
"namoshek/laravel-redis-sentinel": "^0.5",
"ovh/ovh": "^3.3",
"parsedown/laravel": "^1.2",
"phpunit/phpunit": "^10.0",
"propaganistas/laravel-phone": "^5.3",
"ovh/ovh": "^3.5",
"parsedown/laravel": "^1.2.1",
"phpunit/phpunit": "^10.5.50",
"propaganistas/laravel-phone": "^5.3.6",
"react/socket": "^1.16",
"respect/validation": "^2.3",
"respect/validation": "^2.4.4",
"rvxlab/hcaptcha": "^5.2",
"sabre/vobject": "^4.5"
"sabre/vobject": "^4.5.7"
},
"require-dev": {
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^7.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/collision": "^8.5",
"phpmd/phpmd": "^2.15",
"squizlabs/php_codesniffer": "^3.7"
"squizlabs/php_codesniffer": "^3.13.2"
},
"config": {
"platform": {
"php": "8.1"
"php": "8.2"
},
"optimize-autoloader": true,
"preferred-install": "dist",

1449
flexiapi/composer.lock generated

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -37,6 +37,12 @@ return [
'recovery_code_expiration_minutes' => env('APP_RECOVERY_CODE_EXPIRATION_MINUTES', 10),
'provisioning_token_expiration_minutes' => env('APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES', 0),
'reset_password_email_token_expiration_minutes' => env('APP_RESET_PASSWORD_EMAIL_TOKEN_EXPIRATION_MINUTES', 1440),
/**
* Temporary toggles
*/
'show_login_counter_temp' => env('APP_SHOW_LOGIN_COUNTER_TEMP', true),
/**
* Amount of minutes before re-authorizing the generation of a new account creation token
*/

Some files were not shown because too many files have changed in this diff Show more