Compare commits

..

120 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
Peio Rigaux
cc45376d7d Fix FLEXIAPI-169
Added missing selinux label to log files and storage directory
Modify spec file to execute scriplets even on update
Dummy commit to test update
Use posttrans instead of post to launch scriptlet of old package before the one of the new package (easier for maintenance)
Fix selinux typo for labelling of log dir
2025-05-20 17:29:34 +02:00
Timothée Jaussoin
801b8bd047 Fix FLEXIAPI-305 Add specific error page for Space Expiration 2025-05-20 16:26:11 +02:00
Jonathan Bartet
477f23123d Fix FLEXIAPI-296 fix registration text 2025-05-19 11:39:55 +02:00
Timothée Jaussoin
97667ada1c Fix FLEXIAPI-298 Tag the 2.0 2025-05-19 07:57:05 +00:00
Timothée Jaussoin
83b5e66644 Fix FLEXIAPI-297 Fix PrId and CallId validations 2025-05-14 17:37:12 +02:00
Timothée Jaussoin
cf0b835ae4 Fix FLEXIAPI-295 Bump Rocky 9 Docker image to make the CI pass 2025-05-13 11:23:44 +02:00
Timothée Jaussoin
b0b6ab2c51 Fix FLEXIAPI-224 Add a console script to send Space Expiration emails 2025-05-13 08:46:46 +00:00
Timothée Jaussoin
48961ea194 Fix FLEXIAPI-293 Remove the (long) outdated general documentation 2025-05-05 15:59:10 +02:00
Timothée Jaussoin
cd3b9b818b Fix FLEXIAPI-286 Send an account_recovery_token using a push notification 2025-05-05 13:43:44 +00:00
Timothée Jaussoin
682b0ae67b Fix FLEXIAPI-287 Refactor the emails templates 2025-04-29 09:12:59 +00:00
Timothée Jaussoin
0160779784 Fix FLEXIAPI-232 Add provisioning email 2025-04-23 13:01:45 +00:00
Timothée Jaussoin
e2f40699fb Fix FLEXIAPI-205 Remove the deprecated endpoints, compatibility code... 2025-04-22 12:51:15 +00:00
Timothée Jaussoin
a3861304cc Fix FLEXIAPI-284 Add configurable admin API Keys 2025-04-10 09:11:26 +00:00
Timothée Jaussoin
672d6291b7 Fix FLEXIAPI-272 Add Space based email server integration 2025-04-08 09:02:35 +00:00
Timothée Jaussoin
fd0fcd7045 Fix FLEXIAPI-277 Restrict authorized ini keys that can be set to prevent... 2025-04-02 09:25:56 +00:00
Timothée Jaussoin
b493e9006e Fix FLEXIAPI-281 Restrict external_domains unicity on username, domain 2025-03-26 10:25:42 +00:00
Timothée Jaussoin
deeea0ddb6 Fix FLEXIAPI-280 Ensure that the foreign external_accounts key is handling delete on cascade 2025-03-25 14:50:55 +00:00
Timothée Jaussoin
7cb63f3e51 Fix FLEXIAPI-233 Add External Accounts (new version) 2025-03-24 13:36:20 +00:00
Timothée Jaussoin
a8e81908ee Fix FLEXIAPI-278 Complete and reorganize the Markdown documentation 2025-03-18 17:06:34 +01:00
Timothée Jaussoin
1d3c3b8c13 Fix FLEXIAPI-276 Complete the translations 2025-03-13 13:50:10 +00:00
Timothée Jaussoin
11a9f87f1d Fix FLEXIAPI-275 Add names in Spaces 2025-03-05 15:48:55 +01:00
Timothée Jaussoin
09f6e1fa6d Fix FLEXIAPI-261 Remove the TURN part in the XML provisioning (and only keep the API endpoint) 2025-02-26 10:49:15 +01:00
Timothée Jaussoin
f566bc0c7c Fix FLEXIAPI-237 Add internationalisation support in the app 2025-02-26 10:06:55 +01:00
Timothée Jaussoin
cc9fb24db1 Fix FLEXIAPI-271 Handle properly reversed attributes in objects 2025-02-18 12:04:25 +01:00
Timothée Jaussoin
ac74ad31f4 Fix FLEXIAPI-270 Call the static $apnsTypes attribute in FlexisipPusherConnector 2025-02-18 10:57:29 +00:00
Timothée Jaussoin
9006bc1d0d Fix FLEXIAPI-258 Move DotEnv instance configurations in the Spaces table 2025-02-17 14:02:06 +00:00
Timothée Jaussoin
b66cc28004 Fix FLEXIAPI-269 Update the IsNotPhoneNumber rule to use a better phone number validator 2025-02-13 11:58:44 +01:00
Timothée Jaussoin
2bf8db6bd1 Fix FLEXIAPI-268 Allow pn-param in Apple format for the push notifications endpoints 2025-02-13 10:18:03 +01:00
Timothée Jaussoin
63c1c404a6 Fix FLEXIAPI-256 Publish an empty string while deleting a device on Redis to... 2025-02-10 10:47:09 +00:00
Timothée Jaussoin
1ba3834f40 Fix FLEXIAPI-264 Add -k|api_key_ip parameter to accounts:create-admin-account... 2025-02-03 16:24:39 +00:00
Timothée Jaussoin
1b1df7eef8 Fix FLEXIAPI-262 Bypass the JWT auth if we have an API 2025-01-30 10:23:06 +00:00
Timothée Jaussoin
0d48ff3964 Fix FLEXIAPI-260 Return 404 and not 403 if the contact is already in the list... 2025-01-28 14:27:26 +00:00
Timothée Jaussoin
07458db5c9 Fix FLEXIAPI-257 Return a more coherent message when search API endpoints returns a 404 2025-01-07 14:48:59 +01:00
Timothée Jaussoin
786258da1f Fix FLEXIAPI-255 Create a INSTALL.md tutorial and log FlexisipPusherConnector errors 2025-01-06 10:51:18 +01:00
Timothée Jaussoin
0dcb74ef19 Fix FLEXIAPI-254 Allow no data on POST requests to not trigger the ValidateJSON middleware 2024-12-30 17:22:03 +01:00
Timothée Jaussoin
e761e03309 Fix FLEXIAPI-252 Update the hCaptcha Laravel library, use file instead of... 2024-12-18 15:27:43 +00:00
Timothée Jaussoin
5f0595bbaa Fix FLEXIAPI-244 Remove faulty middleware 2024-12-17 10:34:50 +00:00
Timothée Jaussoin
d86e297b81 Fix FLEXIAPI-251 Restrict UI elements not accessible by standard Admins or Users 2024-12-16 14:51:03 +00:00
Timothée Jaussoin
4add0d7daa Fix FLEXIAPI-250 Allow Spaces to be declared without a subdomain 2024-12-16 13:56:14 +00:00
Timothée Jaussoin
9cb24cad77 Fix FLEXIAPI-241 Add a /push-notification endpoint to send custom push... 2024-12-10 16:05:19 +00:00
Timothée Jaussoin
694265cc1c Fix FLEXIAPI-242 Add stricter validation for the AccountCreationToken Push Notification endpoint 2024-12-10 14:26:53 +01:00
Timothée Jaussoin
c310ee0566 Fix GH-15 Add password import from CSV 2024-12-09 11:50:50 +01:00
Timothée Jaussoin
3d715afc23 Fix FLEXIAPI-228 Add reset password email flow 2024-12-04 16:25:13 +01:00
Timothée Jaussoin
93c98ae73f Fix FLEXIAPI-220 Migrate SIP Domains to Spaces 2024-12-02 13:04:49 +00:00
415 changed files with 13786 additions and 7985 deletions

View file

@ -1,7 +1,7 @@
rocky8-deploy:
extends: .deploy
script:
- ./deploy_packages.sh rockylinux
- ./deploy_packages.sh rockylinux 8
needs:
- rocky8-package
- rocky8-test
@ -9,18 +9,18 @@ rocky8-deploy:
rocky9-deploy:
extends: .deploy
script:
- ./deploy_packages.sh rockylinux
- ./deploy_packages.sh rockylinux 9
needs:
- rocky9-package
- rocky9-test
debian11-deploy:
rocky10-deploy:
extends: .deploy
script:
- ./deploy_packages.sh debian bullseye
- ./deploy_packages.sh rockylinux 10
needs:
- debian11-package
- debian11-test
- rocky10-package
- rocky10-test
debian12-deploy:
extends: .deploy
@ -30,28 +30,30 @@ 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
script:
- ./deploy_packages.sh rockylinux
- ./deploy_packages.sh rockylinux 8
needs:
- remi-rocky8-package
- remi-rocky8-test
remi-rocky9-deploy:
extends: .deploy
script:
- ./deploy_packages.sh rockylinux
needs:
- remi-rocky9-package
- remi-rocky9-test
.deploy:
stage: deploy
tags: ["docker"]
only:
- master
- /^release/.*$/
rules:
- if: $CI_COMMIT_REF_NAME == "master"
- if: $CI_COMMIT_REF_NAME =~ /^release/
before_script:
- rm -f $CI_PROJECT_DIR/build/*devel*.rpm # Remove devel packages

View file

@ -1,39 +1,43 @@
rocky8-package:
needs:
- prepare-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 rpm-el8
- make package-el8
rocky9-package:
needs:
- prepare-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 rpm-el9
- make package-el9
debian11-package:
extends: .debian_package
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
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:
- make deb
- make package-deb
remi-rocky8-package:
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
@ -44,16 +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 install redis
.remi-rocky-package:
extends: .package
rules:
- if: $CI_COMMIT_REF_NAME =~ /^release/ || $CI_COMMIT_REF_NAME == "master"
#- changes:
# - .gitlab-ci.yml
script:
# Remi
- mkdir -p $CI_PROJECT_DIR/build
@ -91,7 +91,13 @@ remi-rocky9-package:
- build/*
when: always
expire_in: 1 day
variables:
COMPOSER_CACHE_DIR: $CI_PROJECT_DIR/.composer/cache
COMPOSER_ALLOW_SUPERUSER: 1
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- flexiapi/vendor/
- .composer/cache

View file

@ -0,0 +1,11 @@
prepare-package:
tags: ["docker"]
stage: prepare-package
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION
script:
- make prepare-common
artifacts:
paths:
- rpmbuild/*
when: always
expire_in: 1 day

View file

@ -3,70 +3,54 @@ rocky8-test:
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
needs:
- rocky8-package
- remi-rocky8-package
rocky9-test:
extends: .rocky-test
image: gitlab.linphone.org:4567/bc/public/docker/rocky9-php:$ROCKY_9_IMAGE_VERSION
needs:
- rocky9-package
- remi-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:
- ls build
- 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
- vendor/bin/phpunit --log-junit $CI_PROJECT_DIR/flexiapi_phpunit.log
debian11-test:
extends: .debian-test
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
needs:
- debian11-package
debian12-test:
extends: .debian-test
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION
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
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php
- php -r "unlink('composer-setup.php');"
- php composer.phar install
- composer install
- vendor/bin/phpcs
- vendor/bin/phpmd . ansi phpmd.xml
- php artisan key:generate
- vendor/bin/phpunit --log-junit $CI_PROJECT_DIR/flexiapi_phpunit.log
remi-rocky8-test:
extends: .test
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
needs:
- remi-rocky8-package
script:
- yum -y localinstall build/*.rpm
remi-rocky9-test:
extends: .test
image: gitlab.linphone.org:4567/bc/public/docker/rocky9-php:$ROCKY_9_IMAGE_VERSION
needs:
- remi-rocky9-package
script:
- yum -y localinstall build/*.rpm
mysql-latest-test:
extends: .test
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION

View file

@ -1,18 +1,21 @@
variables:
ROCKY_8_IMAGE_VERSION: 20241113_143521_update_php_82
ROCKY_9_IMAGE_VERSION: 20241114_161138_remove_redis
DEBIAN_11_IMAGE_VERSION: 20241112_113527_update_package_and_dependencies
DEBIAN_12_IMAGE_VERSION: 20241112_113948_update_package_and_dependencies
ROCKY_8_IMAGE_VERSION: 20250702_171834_update_rocky8_dockerhub
ROCKY_9_IMAGE_VERSION: 20250702_171314_update_rocky9_dockerhub
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
include:
- '.gitlab-ci-files/prepare-package.yml'
- '.gitlab-ci-files/package.yml'
- '.gitlab-ci-files/test.yml'
- '.gitlab-ci-files/deploy.yml'
stages:
- prepare-package
- package
- test
- deploy

View file

@ -1,164 +1,157 @@
# Flexisip Account Manager Changelog
# Releases
v1.7
----
- Fix FLEXIAPI-206 Upgrade to Laravel 10, PHP 8.1 minimum and bump all the related dependencies
All notable changes to this project will be documented in this file.
v1.6
----
- Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app
- Fix FLEXIAPI-196 Add a phone validation system by country code with configuration panels and related tests and documentation
- Fix FLEXIAPI-203 Implement domain based Linphone configuration, add documentation, complete API endpoints, complete provisioning XML
- Fix FLEXIAPI-208 Add SMS templates documentation
- Fix FLEXIAPI-211 Add a JSON validation middleware + test
- Fix FLEXIAPI-212 Add CoTURN credentials support in the provisioning
- Fix FLEXIAPI-213 Add TURN credentials support in the API as defined in draft-uberti-behave-turn-rest-00
- Fix FLEXIAPI-216 Implement the RFC 8898 partially... for HTTP
- Fix FLEXIAPI-239 Ensure to return the correct error codes as stated in the RFC6750 section 3.1
- Fix FLEXIAPI-238 Replace Material Icons with Phosphor
- Fix FLEXIAPI-240 Update the Docker images
The format is based on [Keep a Changelog](https://keepachangelog.com/).
v1.5
---
- Fix FLEXIAPI-202 Add account parameter to the redirection in the destroy admin route
- Fix FLEXIAPI-195 Fix LiblinphoneTesterAccoutSeeder to fit with the latest Account related changes
- Fix FLEXIAPI-193 Typo
- Fix FLEXIAPI-192 Clear and upgrade properly the account dictionary entries if the entries are already existing
- Fix FLEXIAPI-191 Add quotes for the pn-prid parameter in FlexisipPusherConnector
- Fix FLEXIAPI-186 Ensure that empty objects are serialized in JSON as objects and not empty arrays
- Fix FLEXIAPI-185 Return null if the account dictionary is empty in the API
- Fix FLEXIAPI-184 Append phone_change_code and email_change_code to the admin /accounts/<id> endpoint if they are available
- Fix FLEXIAPI-183 Complete the account hooks on the dictionnary actions
- Fix FLEXIAPI-182 Replace APP_SUPER_ADMINS_SIP_DOMAINS with a proper sip_domains table, API endpoints, UI panels, console command, tests and documentation
- Fix FLEXIAPI-181 Replace APP_ADMINS_MANAGE_MULTI_DOMAINS with APP_SUPER_ADMINS_SIP_DOMAINS
- Fix FLEXIAPI-180 Fix the token and activation flow for the provisioning with token endpoint when the header is missing
- Fix FLEXIAPI-179 Add Localization support as a Middleware that handles Accept-Language HTTP header
- Fix FLEXIAPI-178 Show the unused code in the Activity tab of the accounts in the admin panel
- Fix FLEXIAPI-177 Complete vcards-storage and devices related endpoints with their User/Admin ones
- Fix FLEXIAPI-176 Improve logs for the deprecated endpoints and AccountCreationToken related serialization
- Fix FLEXIAPI-175 and FLEXISIP-231 Rewrite the Redis contacts parser to handle properly SIP uris (thanks @thibault.lemaire !)
- Fix FLEXIAPI-174 Check if the phone is valid before trying to recover it (deprecated endpoint)
- Fix FLEXIAPI-173 Wrong route in validateEmail (deprecated)
- Fix FLEXIAPI-171 Fix README documentation for CreateAdminAccount
- Fix FLEXIAPI-170 Fix undefined variable apiKey in CreateAdminAccount
- Fix FLEXIAPI-168 Add POST /accounts/me/email to confirm the email change
- Fix FLEXIAPI-167 Add the handling of a custom identifier for the JWT tokens on top of the email one
- Fix FLEXIAPI-166 Reimplement the deprecated email validation URL
- Fix FLEXIAPI-165 Remove for now text/vcard header constraint
- Fix FLEXIAPI-164 Add vcards-storage endpoints
- Fix FLEXIAPI-163 Complete AccountService hooks
- Fix FLEXIAPI-162 Drop the aliases table and migrate the data to the phone column
- Fix FLEXIAPI-161 Complete the Dictionary tests to cover the collection accessor
- Fix FLEXIAPI-159 Add the account_creation_tokens/consume endpoint
- Fix FLEXIAPI-158 Restrict the phone number change API endpoint to return 403 if the account doesn't have a validated Account Creation Token
- Fix FLEXIAPI-156 Disable the Phone change web form when PHONE_AUTHENTICATION is disabled
- Fix FLEXIAPI-155 Add a new accountServiceAccountUpdatedHook and accountServiceAccountDeletedHook
- Fix FLEXIAPI-153 Add phone and email to be changed in the Activity panel
- Fix FLEXIAPI-152 API Key usage clarification
- Fix FLEXIAPI-151 Migrate to hCaptcha
- Fix FLEXIAPI-150 Use the same account_id parameter for both API and Web routes
- Fix FLEXIAPI-149 Add a toggle to disable phone check on username for admin endpoints and forms
- Fix FLEXIAPI-148 Reuse AccountService in the POST /api/accounts admin endpoint
- FIX FLEXIAPI-146 Allow users to manage their own devices
- Fix FLEXIAPI-145 Put back the 'code' parameter as an alias for the 'confirmation_key' for the activateEmail and activatePhone endpoints
- Fix FLEXIAPI-144 Introduce APP_FLEXISIP_PUSHER_FIREBASE_KEYSMAP as a replacement for APP_FLEXISIP_PUSHER_FIREBASE_KEY
- Fix FLEXIAPI-143 JWT Authentication layer on the API
- Fix FLEXIAPI-142 PUT /accounts endpoint doesn't allow overiding values anymore
- Fix FLEXIAPI-140 Fix the display_name attribute in the Vcard4 render
- Fix FLEXIAPI-139 Refactor the email and phone API documentation
- Fix FLEXIAPI-138 Add ip and user_agent columns to all the tokens and code tables, fill the values when required and display them in the admin
- Fix FLEXIAPI-136 Refactor the Web Panel toggle mechanism and move it to a proper Middleware
- Fix FLEXIAPI-135 Merge the admins table in the accounts table
- Fix FLEXIAPI-134 Add a system to detect and block abusive accounts
- Fix FLEXIAPI-133 Use the correct breadcrumb on create and fix a password
- Fix FLEXIAPI-132 Refactor the Provisioning to remove proxy_default_values
- Fix #143 Ensure that the ProvisioningToken model behave likes all the other Consommable
- Fix #141 Add a new hook system for the Account Service
- Fix #138 Add a dictionary attached to the accounts
- Fix #137 Migrate the icons from Material Icons to Material Symbols
- Fix #135 Refactor the password algorithms code
- Fix #134 Create an Activity view in the Admin > Accounts panel
- Fix #133 Make the MySQL connection unstrict
- Fix #132 Move the provisioning_tokens and recovery_codes to dedicated table
- Fix #130 Drop the group column in the Accounts table
## [2.1]
v1.4.9
------
- Complete the missing changelog
### Added
v1.4.8
------
- Fix FLEXIAPI-166 Reimplement the deprecated email validation URL
- Fix FLEXIAPI-140 Select the display_name attribute from the database to inject...
- **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`
v1.4.7
------
- Fix FLEXIAPI-175 and FLEXISIP-231 Rewrite the Redis contacts parser to handle properly SIP uris (thanks @thibault.lemaire !)
### Changed
v1.4.6
------
- Fix FLEXIAPI-142 PUT /accounts endpoint doesn't allow overiding values anymore
- Fix typos and dependencies
- **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**
v1.4.5
------
- Fix FLEXIAPI-132 Refactor the Provisioning to remove proxy_default_values
## [2.0]
v1.4.4
------
- Fix FLEXIAPI-136 Refactor the Web Panel toggle mechanism and move it to a proper Middleware
### Added
v1.4.3
------
- Fix FLEXIAPI-133 Use the correct breadcrumb on create and fix a password update related issue on update
- **Spaces:** A new way to manage your SIP domains and hosts. A Space is defined by a unique SIP Domain and Host pair.
- **New mandatory DotEnv variable** `APP_ROOT_HOST`, replaces `APP_URL` and `APP_SIP_DOMAIN` that are now configured using the new dedicated Artisan script. It defines the root hostname where all the Spaces will be configured. All the Spaces will be as subdomains of `APP_ROOT_HOST` except one that can be equal to `APP_ROOT_HOST`. Example: if `APP_ROOT_HOST=myhost.com` the Spaces hosts will be `myhost.com`, `alpha.myhost.com` , `beta.myhost.com`...
- **New DotEnv variable:** `APP_ACCOUNT_RECOVERY_TOKEN_EXPIRATION_MINUTES=0` Number of minutes before expiring the recovery tokens
- **New Artisan script** `php artisan spaces:create-update {sip_domain} {host} {name} {--super}`, replaces `php artisan sip_domains:create-update {sip_domain} {--super}`. Can create a Space or update a Space Host base on its Space SIP Domain.
- **Push Notification endpoint** Add a /push-notification endpoint to send custom push notifications to the Flexisip Pusher
- **Add internationalisation support in the app** The web panels are now available in French and English
- **Add External Accounts** In the API and web panels, allowing users to setup an external account that can be used in another service like the B2BUA
- **Add configurable admin API Keys** Allowing admins to setup non-expiring services API Keys
- **Add provisioning email** A user can now receive a custom generated email with all the provisioning related information
- **Add API endpoints to send the password reset and provisioning emails**
- **Add an app setup wizard page** Static web page inviting the users to download the app if it is not installed yet
v1.4.2
------
- Fix #135 Refactor the password algorithms code
### Changed
v1.4.1
------
- Fix #133 Make the MySQL connection unstrict
- **Complete and reorganize the Markdown documentation**
- **Refactor the emails templates** All the emails were modernized and are now generated in HTML
v1.4
----
- Redesign and refactoring of the main UI and panel flows
- Complete the statistics and add a specific API to get usage statistics from FlexiAPI
- Removal of XMLRPC
- Add RockyLinux 9 support
- Add Debian 12 to CI
- Fix #122 Add a new console command CreateFirstAdmin
- Fix #121 Only apply throttling to redeemed tokens
- Fix #123 Define a proper documentation for the provisioning flow
- Fix #124 Return 404 when the account is already provisioned or the provisioning_token not valid
- Fix #125 Remove the External Accounts feature
- Fix #19 Set all the ERROR confirmation_key to null in the accounts table
### Removed
v1.3
----
- Fix #90 Deploy packages from release branches as well
- Fix #58 Fix the packaging process to use git describe as a reference
- Fix #58 Move the generated packages in the build directory, and fix the release and version format in the .spec
- Fix #58 Refactor and cleanup the .gitlab-ci file
- Move the minimum PHP version to 8.0
- Fix #47 Move the docker to an external repository
- Fix #83 Add php-redis-remi package
- Fix #85 Also package php-pecl-igbinary and php-pecl-msgpack from remi
- Fix #84 Remove CentOS7 from the pipeline
- Fix #80 Inject provisioning link and QRCode in the default email with a password_reset parameter
- Fix #79 Add a refresh_password parameter to the provisioning URLs
- Fix #78 Add a APP_ACCOUNTS_EMAIL_UNIQUE environnement setting
- Fix #30 Remove APP_EVERYONE_IS_ADMIN
- **Remove the deprecated endpoints** The endpoints inherited from XMLRPC are now completely removed, the following variable can be removed:
- APP_DANGEROUS_ENDPOINTS
- APP_PROJECT_URL
- **Removing and moving DotEnv instance environnement variables to the Spaces** The following DotEnv variables were removed. You can now configure them directly in the designated spaces after the migration.
- INSTANCE_COPYRIGHT
- INSTANCE_INTRO_REGISTRATION
- INSTANCE_CONFIRMED_REGISTRATION_TEXT
- INSTANCE_CUSTOM_THEME
- WEB_PANEL
- PUBLIC_REGISTRATION
- PHONE_AUTHENTICATION
- DEVICES_MANAGEMENT
- INTERCOM_FEATURES
- NEWSLETTER_REGISTRATION_ADDRESS
- ACCOUNT_PROXY_REGISTRAR_ADDRESS
- ACCOUNT_TRANSPORT_PROTOCOL_TEXT
- ACCOUNT_REALM
- ACCOUNT_PROVISIONING_RC_FILE
- ACCOUNT_PROVISIONING_OVERWRITE_ALL
- ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER
- **Enforce the session and cache in the configuration** The following variables can be removed from your DotEnv file as well:
- SESSION_DRIVER
- CACHE_DRIVER
v1.2
----
### Migrate from [1.6]
- Introduce FlexiAPI built on Laravel to replace XMLRPC
- Deprecates XMLRPC (will be removed in the 2.0 release)
- Create a REST API to manage the accounts, related features and provisioning
- Create a user web panel for their account management, currently in testing phase (unstable)
- Create an admin web panel to manage accounts and related features
- Allow accounts to be exported as ExternalAccounts and imported in another Flexisip Account Manager instance
- Add various artisan console commands to maintain the data (cleaning up, importing, exporting, seeding)
- Add unit tests for the FlexiAIP REST API
- Rebuild the existing database using the Laravel migration scripts
1. Deploy the new version and migrate the database.
```
php artisan migrate
```
2. Set `APP_ROOT_HOST` in `.env` or as an environnement variable. And remove `APP_URL` and `APP_SIP_DOMAIN`
```
APP_ROOT_HOST=myhost.com
```
3. The migration script will automatically copy the `sip_domain` into `host` in the `spaces` table. You then have to "fix" the hosts and set them to equal or be subdomains of `APP_ROOT_HOST`.
```
php artisan spaces:create-update my.sip myhost.com "My Super Space" --super # You can set some Spaces as SuperSpaces, the admin will be able to manage the other spaces
php artisan spaces:create-update alpha.sip alpha.myhost.com "Alpha Space"
php artisan spaces:create-update beta.sip beta.myhost.com "Beta Space"
...
```
4. Configure your web server to point the `APP_ROOT_HOST` and subdomains to the app. See the related documentation in [`INSTALL.md` file](INSTALL.md#31-mandatory-app_root_host-variable).
5. Configure your Spaces.
6. (Optional) Import the old instance DotEnv environnement variables into a space.
7. Remove the instance based environnement variables (see **Changed** above) and configure them directly in the spaces using the API or Web Panel.
⚠️ Be careful, during this import only the project DotEnv file variables will be imported, other environnement (eg. set in Apache, nginx or Docker) will be ignored.
⚠️ The content of the `ACCOUNT_PROVISIONING_RC_FILE` will not be imported. You will have to extract the sections and lines that you want to use manually using the dedicated form or the API.
```
php artisan spaces:import-configuration-from-dot-env {sip_domain}
```
You can find more details regarding those steps in the [`INSTALL.md`](INSTALL.md) and [`README.md`](README.md) files.
## [1.6] - 2024-12-30
### Added
- **Allow the expiration of tokens and codes in the DotEnv configuration**
- **New DotEnv variables:** check all the new `*_EXPIRATION_MINUTES` for each token and code in `.env.example`
- **Phone validation system by country code:** all the provided phone numbers are now properly validated and some countries can be forbidden
- **SIP Domain management:** the account domains are now managed in a set of panels and API endpoints, this is the base of the upcoming space administration system
- **JSON validation in the API:** the provised JSON is now validated and returns an error if an issue is detected
- **CoTURN credentials support:** TURN credentials can now be generated and return through the provisioning feature
- **RFC 8898 Support**
## Changed
- **Replace Material Icons with Phosphor**
## Deprecated
- **Last major version supporting the deprecated endpoints of the API**
### Migrate from [1.5]
Nothing specific to do
## [1.5] - 2024-08-29
### Added
- **Account activity view:** new panel, available behind the Activity tab, will allow any admin to follow the activity of the accounts they manage.
- **Detect and block abusive accounts:** This activity tracking is coming with a related tool that is measuring the accounts activity and automatically block them if it detects some unusual behaviors on the service. An account can also directly be blocked and unblocked from the setting panel. Two new setting variables will allow you to fine tune those behaviors triggers.
- **New DotEnv variable:** `BLOCKING_TIME_PERIOD_CHECK=30` Time span on which the blocking service will proceed, in minutes
- **New DotEnv variable:** `BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD=5` Amount of account events authorized during this period
- **OAuth JWT Authentication:** OAuth support with the handling of JWE tokens issues by a third party service such as Keycloack.
- **New DotEnv variable:** `JWT_RSA_PUBLIC_KEY_PEM=`
- **New DotEnv variable:** `JWT_SIP_IDENTIFIER=sip_identifier`
- **Super-domains and super-admins support:** Introduce SIP domains management. The app accounts are now divided by their domains with their own respective administrators that can only see and manage their own domain accounts and settings. On top of that it is possible to configure a SIP domain as a "super-domain" and then allow its admins to become "super-admins". Those super-admins will then be able to manage all the accounts handled by the instance and create/edit/delete the other SIP domains. Add new endpoints and a new super-admin role in the API to manage the SIP domains. SIP domains can also be created and updated directly from the console using a new artisan script (documented in the README);
- **New Artisan script:** `php artisan sip_domains:create-update {domain} {--super}`
- **Account Dictionary:** Each account can now handle a specific dictionary, configurable by the API or directly the web panel. This dictionary allows developers to store arbitrary `key -> value pairs` on each accounts.
- **Vcard storage:** Attach custom vCards on a dedicated account using new endpoints in the API. The published vCard are validated before being stored.
### Changed
- **User management of their own devices:** Allowing users will be able to manage its own devices. Specific API endpoints were also added to manage them directly from the clients.
- **Migration to hCaptcha:** Migrate from Google Recaptcha to hCaptcha in this release.
- **New DotEnv variable:** HCAPTCHA_SECRET=secret-key
- **New DotEnv variable:** HCAPTCHA_SITEKEY=site-key
- **Localization support:** The API is now accepting the `Accept-Language` header and adapt its internal localization to the client/browser one. For the moment only French and English are supported but more languages could be added in the future.

160
INSTALL.md Normal file
View file

@ -0,0 +1,160 @@
# DotEnv configuration
FlexiAPI relies on [DotEnv](https://github.com/vlucas/phpdotenv) to be configured. This configuration can be accessed using the existing `.env` file that can be itself overwritten by an environnement variables.
Those variables can then be set using Docker-Compose, a bash script or a web-server.
If you're installing FlexiAPI from the RedHat or Debian package you can find the configuration file at `/etc/flexisip-account-manager/flexiapi.env`.
Check also the [RELEASE.md](RELEASE.md) to check if you don't have specific migrations to do between releases.
# 1.a Manual setup
Clone the repository, install the dependencies and generate a key.
composer install --no-dev
php artisan key:generate
# 1.b Packages setup
FlexiAPI is packaged for Debian and RedHat, you can setup those repositories using the Flexisip documentation https://wiki.linphone.org/xwiki/wiki/public/view/Flexisip/1.%20Installation/#HInstallationfromourrepositories
yum install bc-flexisip-account-manager # For RedHat distributions
apt install bc-flexisip-account-manager # For Debian distributions
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.
This file can be loaded and configured in your specific VirtualHost configuration.
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.
## 3.1. Mandatory `APP_ROOT_HOST` variable
`APP_ROOT_HOST` contains the HTTP host where your FlexiAPI is hosted (eg. `flexiapi.domain.tld` or directly `flexiapi-domain.tld`).
This is the host that you'll define in the Apache or webserver VirtualHost:
ServerName flexiapi-domain.tld
ServerAlias *.flexiapi-domain.tld
If you are planning to manage several Spaces (see Spaces bellow) a wildcard `ServerAlias` as above is required.
## 3.2. Database migration
Then configure the database connection parameters and migrate the tables. The first migration *MUST* be run on an empty database.
php artisan migrate
# 4. Spaces
Since the 1.6 FlexiAPI can manage different SIP Domains on separate HTTP subdomains.
A Space is defined as a specific HTTP subdomain of `APP_ROOT_HOST` and is linked to a specific SIP Domain. It is also possible to host one (and only one) specific Space directly under `APP_ROOT_HOST`.
By default administrator accounts in Spaces will only see the accounts of their own Space (that have the same SIP Domain).
However it is possible to define a Space as a "SuperSpace" allowing the admins to see all the other Spaces and accounts and create/edit/delete the other Spaces.
## 4.1. Setup the first Space
You will need to create the first Space manually, generally as a SuperSpace, after that the other Spaces can directly be created in your browser through the Web Panels.
php artisan spaces:create-update {sip_domain} {host} {name} {--super}
For example:
php artisan spaces:create-update company-sip-domain.tld flexiapi-domain.tld "My Super Space" --super
php artisan spaces:create-update other-sip-domain.tld other.flexiapi-domain.tld "My Other Space"
## 5. Create a first administrator and finish the setup
Create a first administrator account:
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=}
For example:
php artisan accounts:create-admin-account -u admin -p strong_password -d company-sip-domain.tld
You can now try to authenticate on the web panel and continue the setup using your admin account.
# Other custom configurations
## Multiple virtualhosts option
In your web server configuration create several VirtualHosts that are pointing to the same FlexiAPI instance.
Using the environnement variables you can then configure FlexiAPI per instance.
With Apache, use the [mod_env](https://httpd.apache.org/docs/2.4/mod/mod_env.html) module.
SetEnv APP_ENV "production"
On nginx use `fastcgi_param` to pass the parameter directly to PHP.
location ~ [^/]\.php(/|$) {
include /etc/nginx/fastcgi_params;
fastcgi_param APP_ENV "staging";
}
> **Warning** Do not create a cache of your configuration (using `artisan config:cache`) if you have a multi-environnement setup.
> The cache is always having the priority on the variables set in the configuration files.
## Custom Theme
If you enable the Custom CSS Theme option to true in the Space administration panel, FlexiAPI will try to load a CSS file located in `public/css/$space_host.style.css`. If the file doesn't exists it will fallback to `public/css/style.css`.
You can find an example CSS file at `public/css/custom.style.css`.
## Flexisip Push notifications pusher
The API endpoint `POST /account_creation_tokens/send-by-push` uses the `flexisip_pusher` binary delivered by the [Flexisip](https://gitlab.linphone.org/BC/public/flexisip) project (and related package). You must configure the `APP_FLEXISIP_PUSHER_PATH` and `APP_FLEXISIP_PUSHER_FIREBASE_KEYSMAP` environnement variables to point to the correct binary.
APP_FLEXISIP_PUSHER_PATH=/opt/belledonne-communications/bin/flexisip_pusher
This binary will be executed under "web user" privileges. Ensure that all the related files required by `flexisip_pusher` can be accessed using this user account.
/var/opt/belledonne-communications/log/flexisip/flexisip-pusher.log // Write permissions
/etc/flexisip/apn/*pem // Read permissions
## SELinux restrictions
If you are running on a RedHat machine, please ensure that SELinux is correctly configured.
Allow the webserver user to write in the `storage/` directory:
chcon -R -t httpd_sys_rw_content_t storage/
Don't forget to make this change persistent if the directory may be relabeled :
semanage fcontext -a -t httpd_sys_rw_content_t storage/
You can use the restorecon command to verify that this is working :
restorecon storage/
If your database is located on a remote machine, you should also allow your webserver user to connect to remote hosts:
semanage port -a -t http_port_t -p tcp 3306 // Open remote connections on the MySQL port for example
setsebool -P httpd_can_network_connect 1 // Allow remote network connected
setsebool -P httpd_can_network_connect_db 1 // Allow remote database connection
If you are planning to send emails using your account manager:
setsebool -P httpd_can_sendmail 1 // Allow email to be sent

View file

@ -20,7 +20,7 @@ else
endif
cleanup-package-semvers:
rm flexisip-account-manager.spec.run
rm -f flexisip-account-manager.spec.run
prepare:
cd flexiapi && php composer.phar install --ignore-platform-req=ext-redis --no-dev
@ -38,6 +38,7 @@ package-common:
cp -R --parents flexiapi/**/* $(OUTPUT_DIR)/flexisip-account-manager/
cp flexiapi/composer* $(OUTPUT_DIR)/flexisip-account-manager/flexiapi/
cp README.md $(OUTPUT_DIR)/flexisip-account-manager/
cp INSTALL.md $(OUTPUT_DIR)/flexisip-account-manager/
cp flexiapi/.env.example $(OUTPUT_DIR)/flexisip-account-manager/flexiapi/.env.example
cp flexiapi/artisan $(OUTPUT_DIR)/flexisip-account-manager/flexiapi/
cp flexiapi/phpunit.xml $(OUTPUT_DIR)/flexisip-account-manager/flexiapi/
@ -49,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:
@ -57,25 +58,34 @@ package-end-common:
rm -rf $(OUTPUT_DIR)/rpmbuild/SPECS $(OUTPUT_DIR)/rpmbuild/SOURCES $(OUTPUT_DIR)/rpmbuild/SRPMS $(OUTPUT_DIR)/rpmbuild/BUILD $(OUTPUT_DIR)/rpmbuild/BUILDROOT
rpm-el8-only:
sed -i 's/Requires:.*/Requires: php >= 8.0, 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
mkdir -p build
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:
rpmbuild -v -bb --define 'dist .el9' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
mkdir -p build
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/.
rm -r rpmbuild
deb-only:
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
mkdir -p build
sed -i 's/posttrans/post/g' $(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.0), 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"
@ -86,11 +96,22 @@ deb-only:
mv *.deb build/.
rpm-el8: prepare package-semvers package-common rpm-el8-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el8-dev: prepare-dev package-semvers package-common rpm-el8-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el9: prepare package-semvers package-common rpm-el9-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el9-dev: prepare-dev package-semvers package-common rpm-el9-only rpm-cleanup cleanup-package-semvers package-end-common
deb: prepare package-semvers package-common deb-only cleanup-package-semvers package-end-common
deb-dev: prepare-dev package-semvers package-common deb-only cleanup-package-semvers package-end-common
prepare-common: prepare package-semvers package-common
package-el8: rpm-el8-only rpm-cleanup cleanup-package-semvers package-end-common
rpm-el8: prepare-common package-el8
rpm-el8-dev: prepare-dev package-semvers package-common package-el8
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
.PHONY: rpm

151
README.md
View file

@ -17,130 +17,14 @@ Flexisip is dual licensed, and can be licensed and distributed:
# Documentation
Once deployed you can have access to the global and API documentation on the `/api` and `/documentation` pages.
Once deployed you can have access to the global and API documentation on the `/api` and `/provisioning/documentation` pages.
# Setup
## DotEnv configuration
FlexiAPI relies on [DotEnv](https://github.com/vlucas/phpdotenv) to be configured. This configuration can be accessed using the existing `.env` file that can be itself overwritten by an environnement variables.
Thoses variables can then be set using Docker-Compose, a bash script or a web-server for example.
If you're installing FlexiAPI from the RPM package you can find the configuration file at `/etc/flexisip-account-manager/flexiapi.env`.
## Manual setup
Clone the repository, install the dependencies and generate a key.
composer install --no-dev
php artisan key:generate
Then configure the database connection in the `.env` file (from the `.env.example` one). And migrate the tables. The migration *MUST* be run on an empty database. The `.env` file will be available at the root of the project or often located in `/etc/flexisip-account-manager` in packaged versions.
php artisan migrate
You can also run the test suit using `phpunit`.
To know more about the web server configuration part, you can directly [visit the official Laravel installation documentation](https://laravel.com/docs/8.x).
### Apache2 server configuration
The package will deploy a `flexisip-account-manager.conf` file in the apache2 configuration directory.
This file can be loaded and configured in your specific VirtualHost configuration.
### Configure the .env file
Complete all the other variables in the `.env` file or by overwritting them in your Docker or web-server configuration:
- The OVH SMS connector
- SMTP configuration
- App name, SIP domain…
### Multi instances environement
FlexiAPI can also handle multi domains setup.
#### Multiple virtualhosts option
In your web server configuration create several virtualhosts that are pointing to the same FlexiAPI instance.
Using the environnement variables you can then configure FlexiAPI per instance.
With Apache, use the [mod_env](https://httpd.apache.org/docs/2.4/mod/mod_env.html) module.
SetEnv APP_NAME "VirtualHost One"
On nginx use `fastcgi_param` to pass the parameter directly to PHP.
location ~ [^/]\.php(/|$) {
include /etc/nginx/fastcgi_params;
fastcgi_param APP_NAME "VirtualHost Two";
}
> **Warning** Do not create a cache of your configuration (using `artisan config:cache`) if you have a multi-environnement setup.
> The cache is always having the priority on the variables set in the configuration files.
#### Multiple .env option
To do so, configure several web servers virtualhosts and set a specific `APP_ENV` environnement variable in each of them.
Note that if `APP_ENV` is not set FlexiAPI will directly use the default `.env` file.
FlexiAPI will then try to load a custom configuration file with the following name `.env.$APP_ENV`. So for the previous example `.env.foobar`.
You can then configure your instances with specific values.
INSTANCE_COPYRIGHT="FooBar - Since 1997"
INSTANCE_INTRO_REGISTRATION="Welcome on the FooBar Server"
INSTANCE_CUSTOM_THEME=true
#### Custom theme
If you set `INSTANCE_CUSTOM_THEME` to true, FlexiAPI will try to load a CSS file located in `public/css/$APP_ENV.style.css`. If the file doesn't exists it will fallback to `public/css/style.css`.
You can find an example CSS file at `public/css/custom.style.css`.
#### Flexisip Push notifications pusher
The API endpoint `POST /account_creation_tokens/send-by-push` uses the `flexisip_pusher` binary delivered by the [Flexisip](https://gitlab.linphone.org/BC/public/flexisip) project (and related package). You must configure the `APP_FLEXISIP_PUSHER_PATH` and `APP_FLEXISIP_PUSHER_FIREBASE_KEYSMAP` environnement variables to point to the correct binary.
APP_FLEXISIP_PUSHER_PATH=/opt/belledonne-communications/bin/flexisip_pusher
This binary will be executed under "web user" privileges. Ensure that all the related files required by `flexisip_pusher` can be accessed using this user account.
/var/opt/belledonne-communications/log/flexisip/flexisip-pusher.log // Write permissions
/etc/flexisip/apn/*pem // Read permissions
### SELinux restrictions
If you are running on a CentOS/RedHat machine, please ensure that SELinux is correctly configured.
Allow the webserver user to write in the `storage/` directory:
chcon -R -t httpd_sys_rw_content_t storage/
Don't forget to make this change persistent if the directory may be relabeled :
semanage fcontext -a -t httpd_sys_rw_content_t storage/
You can use the restorecon command to verify that this is working :
restorecon storage/
If your database is located on a remote machine, you should also allow your webserver user to connect to remote hosts:
semanage port -a -t http_port_t -p tcp 3306 // Open remote connections on the MySQL port for example
setsebool -P httpd_can_network_connect 1 // Allow remote network connected
setsebool -P httpd_can_network_connect_db 1 // Allow remote database connection
If you are planning to send emails using your account manager:
setsebool -P httpd_can_sendmail 1 // Allow email to be sent
Check the [INSTALL.md](INSTALL.md) and [CHANGELOG.md](CHANGELOG.md) files.
## Usage
For the web panel, a general documentation is available under the `/documentation` page.
For the REST API, the `/api` page contains all the required documentation to authenticate and request the API.
FlexiAPI is also providing endpoints to provision Liblinphone powered devices. You can find more documentation about it on the `/provisioning/documentation` documentation page.
@ -148,19 +32,29 @@ FlexiAPI is also providing endpoints to provision Liblinphone powered devices. Y
FlexiAPI is shipped with several console commands that you can launch using the `artisan` executable available at the root of this project.
### Create or update a SIP Domain
### Create or update a Space
Create or update a SIP Domain, required to then create accounts afterward. The `super` option enable/disable the domain as a super domain.
Create or update a Space, required to then create accounts afterward. The `super` option enable/disable the domain as a super domain.
php artisan sip_domains:create-update {domain} {--super}
php artisan spaces:create-update {sip_domain} {host} {name} {--super}
### Import the old DotEnv instance configuration into a Space
Since 2.0 some environnement instance configuration variables were moved into the Space configuration, you can import them using this command.
php artisan spaces:import-configuration-from-dot-env {sip_domain}
⚠️ Be careful, during this import only the project DotEnv file variables will be imported, other environnement (eg. set in Apache, nginx or Docker) will be ignored.
⚠️ The content of the `ACCOUNT_PROVISIONING_RC_FILE` will not be imported. You will have to extract the sections and lines that you want to use manually using the dedicated form or the API.
### Create an admin account
Create an admin account, an API Key will also be generated along the way, it might expire after a while.
Create an admin account, an API Key will also be generated along the way, it might expire after a while (regarding the API Key expiration policy). An empty `api_key_ip` will remove the IP restriction on the key.
If no parameters are put, a default admin account will be created.
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=}
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=} {-k|api_key_ip=}
### Clear the expired API Keys
@ -200,22 +94,19 @@ Once one account is declared as administrator, you can directly configure the ot
### Seed liblinphone test accounts
You can also seed the tables with test accounts for the liblinphone test suite with the following command (check LiblinphoneTesterAccoutSeeder for the JSON syntax):
You can also seed the tables with test accounts for the liblinphone test suite with the following command (check LiblinphoneTesterAccountSeeder for the JSON syntax):
php artisan accounts:seed /path/to/accounts.json
## SMS templates
To send SMS to the USA some providers need to validate their templates before transfering them, see [Sending SMS messages to the USA - OVH](https://help.ovhcloud.com/csm/en-ie-sms-sending-sms-to-usa?id=kb_article_view&sysparm_article=KB0051359).
To send SMS to the USA some providers need to validate their templates before transferring them, see [Sending SMS messages to the USA - OVH](https://help.ovhcloud.com/csm/en-ie-sms-sending-sms-to-usa?id=kb_article_view&sysparm_article=KB0051359).
Here are the currently used SMS templates in the app to declare in your provider panel:
- Creation code: `Your #APP_NAME# creation code is #CODE#`. Sent to confirm the creation of the account by SMS.
- Recovery code: `Your #APP_NAME# recovery code is #CODE#`. Sent to recover the account by SMS.
- Validation code: `Your #APP_NAME# validation code is #CODE#`. Sent to validate the phone change by SMS.
- Validation code with expiration: `Your #APP_NAME# validation code is #CODE#. The code is available for #CODE_MINUTES# minutes`. Sent to validate the phone change by SMS, include an expiration time.
## Custom email templaces
## Custom email templates
Some email templates can be customized.
@ -246,7 +137,7 @@ The `POST /api/messages` endpoint allows you to send messages on the SIP network
APP_LINPHONE_DAEMON_UNIX_PATH=/tmp/ld
If you have issues connecting to that socket check the [`systemd restrictions`](#systemd-restrictions) part of this document.
If you have issues connecting to that socket check the [`systemd restrictions`](INSTALL.md#systemd-restrictions) part of this document.
The socket is located in the `/tmp` directory.

View file

@ -1,35 +0,0 @@
# Releases
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [1.5] - 2024-08-29
### Added
- **Account activity view:** new panel, available behind the Activity tab, will allow any admin to follow the activity of the accounts they manage.
- **Detect and block abusive accounts:** This activity tracking is coming with a related tool that is measuring the accounts activity and automatically block them if it detects some unusual behaviors on the service. An account can also directly be blocked and unblocked from the setting panel. Two new setting variables will allow you to fine tune those behaviors triggers.
- **New DotEnv variable:** `BLOCKING_TIME_PERIOD_CHECK=30` # Time span on which the blocking service will proceed, in minutes
- **New DotEnv variable:** `BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD=5` # Amount of account events authorized during this period
- **OAuth JWT Authentication:** OAuth support with the handling of JWE tokens issues by a third party service such as Keycloack.
- **New DotEnv variable:** `JWT_RSA_PUBLIC_KEY_PEM=`
- **New DotEnv variable:** `JWT_SIP_IDENTIFIER=sip_identifier`
- **Super-domains and super-admins support:** Introduce SIP domains management. The app accounts are now divided by their domains with their own respective administrators that can only see and manage their own domain accounts and settings. On top of that it is possible to configure a SIP domain as a "super-domain" and then allow its admins to become "super-admins". Those super-admins will then be able to manage all the accounts handled by the instance and create/edit/delete the other SIP domains. Add new endpoints and a new super-admin role in the API to manage the SIP domains. SIP domains can also be created and updated directly from the console using a new artisan script (documented in the README);
- **New Artisan script:** `php artisan sip_domains:create-update {domain} {--super}`
- **Account Dictionary:** Each account can now handle a specific dictionary, configurable by the API or directly the web panel. This dictionary allows developers to store arbitrary `key -> value pairs` on each accounts.
- **Vcard storage:** Attach custom vCards on a dedicated account using new endpoints in the API. The published vCard are validated before being stored.
### Changed
- **User management of their own devices:** Allowing users will be able to manage its own devices. Specific API endpoints were also added to manage them directly from the clients.
- **Migration to hCaptcha:** Migrate from Google Recaptcha to hCaptcha in this release.
- **New DotEnv variable:** HCAPTCHA_SECRET=secret-key
- **New DotEnv variable:** HCAPTCHA_SITEKEY=site-key
- **Localization support:** The API is now accepting the `Accept-Language` header and adapt its internal localization to the client/browser one. For the moment only French and English are supported but more languages could be added in the future.
### Deprecated
- **Last major version supporting the deprecated endpoints of the API**

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,7 +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-unconfirmed 30 --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 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,7 +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-unconfirmed 30 --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

View file

@ -1,9 +1,7 @@
APP_NAME=FlexiAPI
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_SIP_DOMAIN=sip.example.com
APP_ROOT_HOST=
APP_LINPHONE_DAEMON_UNIX_PATH=
APP_FLEXISIP_PUSHER_PATH=
@ -11,41 +9,20 @@ APP_FLEXISIP_PUSHER_FIREBASE_KEYSMAP= # Each pair is separated using a space and
APP_ALLOW_PHONE_NUMBER_USERNAME_ADMIN_API=false # Allow phone numbers to be set as username in admin account creation endpoints
# Instance specific parameters
INSTANCE_COPYRIGHT= # Simple text displayed in the page footer
INSTANCE_INTRO_REGISTRATION= # Markdown text displayed in the home page
INSTANCE_CUSTOM_THEME=false
INSTANCE_CONFIRMED_REGISTRATION_TEXT= # Markdown text displayed when an account is confirmed
WEB_PANEL=true # Fully enable/disable the web panels
PUBLIC_REGISTRATION=true # Toggle to enable/disable the public registration forms
PHONE_AUTHENTICATION=true # Toggle to enable/disable the SMS support, requires public registration
DEVICES_MANAGEMENT=false # Toggle to enable/disable the devices management supporttrue
INTERCOM_FEATURES=false # Toggle to enable/disable the intercom related features
NEWSLETTER_REGISTRATION_ADDRESS= # Address to contact when a user wants to register to the newsletter
TERMS_OF_USE_URL= # A URL pointing to the Terms of Use
PRIVACY_POLICY_URL= # A URL pointing to the Privacy Policy
APP_PROJECT_URL= # A URL pointing to the project information page
LOG_CHANNEL=stack
# Risky toggles
APP_DANGEROUS_ENDPOINTS=false # Enable some dangerous endpoints used for XMLRPC like fallback usage
# SIP server parameters
ACCOUNT_PROXY_REGISTRAR_ADDRESS=sip.example.com # Proxy registrar address, can be different than the SIP domain
ACCOUNT_TRANSPORT_PROTOCOL_TEXT="TLS (recommended), TCP or UDP" # Simple text, to explain how the SIP server can be reached
ACCOUNT_REALM=null # Default realm for the accounts, fallback to the domain if not set, enforce null by default
# Expiration time for tokens and code, in minutes, 0 means no expiration
APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two consecutive account_creation_token creation
APP_ACCOUNT_CREATION_TOKEN_EXPIRATION_MINUTES=0
APP_ACCOUNT_RECOVERY_TOKEN_EXPIRATION_MINUTES=0
APP_EMAIL_CHANGE_CODE_EXPIRATION_MINUTES=10
APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES=10
APP_RECOVERY_CODE_EXPIRATION_MINUTES=10
APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES=0
APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the unused API Keys are valid
APP_RESET_PASSWORD_EMAIL_TOKEN_EXPIRATION_MINUTES=1440 # 24h
# Account creation and authentication
ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts
@ -54,11 +31,6 @@ ACCOUNT_USERNAME_REGEX="^[a-z0-9+_.-]*$"
ACCOUNT_DEFAULT_PASSWORD_ALGORITHM=SHA-256 # Can ONLY be MD5 or SHA-256 in capital, default to SHA-256
ACCOUNT_AUTHENTICATION_BEARER= # Bearer value (WWW-Authenticate: Bearer <value>) of the external service that can provide a trusted (eg. JWT token) for the authentication, takes priority and disable the DIGEST auth if set, see https://www.rfc-editor.org/rfc/rfc8898
# Account provisioning
ACCOUNT_PROVISIONING_RC_FILE=
ACCOUNT_PROVISIONING_OVERWRITE_ALL=
ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER=true
# Blocking service
BLOCKING_TIME_PERIOD_CHECK=30 # Time span on which the blocking service will proceed, in minutes
BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD=5 # Amount of account events authorized during this period
@ -86,10 +58,9 @@ REDIS_DB=
# Logs
# Ensure that you have the proper SELinux configuration to write in the storage directory, see the README
LOG_CHANNEL=stack
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
# SMTP and emails
@ -107,7 +78,6 @@ MAIL_VERIFY_PEER_NAME=true
MAIL_SIGNATURE="The Example Team"
# CoTURN
COTURN_SERVER_HOST= # IP or domain name
COTURN_SESSION_TTL_MINUTES=1440 # 60 * 24
COTURN_STATIC_AUTH_SECRET= # static-auth-secret in the coturn configuration
@ -127,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,9 +36,9 @@ class Account extends Authenticatable
use HasFactory;
use Compoships;
protected $with = ['passwords', 'activationExpiration', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries'];
protected $hidden = ['expire_time', 'confirmation_key', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries'];
protected $appends = ['realm', 'confirmation_key_expires', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary'];
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 = [
'activated' => 'boolean',
];
@ -73,7 +73,10 @@ class Account extends Authenticatable
return;
}
$builder->where('domain', config('app.sip_domain'));
/**
* config('app.sip_domain') is required for the Tests suit
*/
$builder->where('domain', config('app.sip_domain') ?? space()->domain);
});
}
@ -111,14 +114,19 @@ class Account extends Authenticatable
});
}
public function activationExpiration()
{
return $this->hasOne(ActivationExpiration::class);
}
public function apiKey()
{
return $this->hasOne(ApiKey::class);
return $this->hasOne(ApiKey::class)->whereNull('expires_after_last_used_minutes');
}
public function adminApiKeys()
{
return $this->hasMany(ApiKey::class)->whereNotNull('expires_after_last_used_minutes');
}
public function external()
{
return $this->hasOne(ExternalAccount::class);
}
public function contacts()
@ -126,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);
@ -141,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;
@ -178,9 +209,9 @@ class Account extends Authenticatable
return $this->belongsToMany(AccountType::class);
}
public function sipDomain()
public function space()
{
return $this->hasOne(SipDomain::class, 'domain', 'domain');
return $this->hasOne(Space::class, 'domain', 'domain');
}
public function statisticsFromCalls()
@ -251,11 +282,38 @@ class Account extends Authenticatable
return $this->hasOne(AccountCreationToken::class);
}
public function accountRecoveryTokens()
{
return $this->hasMany(AccountRecoveryToken::class);
}
public function authTokens()
{
return $this->hasMany(AuthToken::class);
}
/**
* Reset password
*/
public function getCurrentResetPasswordUrlAttribute(): string
{
return replaceHost(
route('account.reset_password_email.change', $this->currentResetPasswordEmailToken->token),
$this->space->host
);
}
public function currentResetPasswordEmailToken()
{
return $this->hasOne(ResetPasswordEmailToken::class)->where('used', false)->latestOfMany();
}
public function resetPasswordEmailTokens()
{
return $this->hasMany(ResetPasswordEmailToken::class)->latest();
}
/**
* Attributes
*/
@ -286,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'];
@ -302,12 +369,12 @@ class Account extends Authenticatable
public function getRealmAttribute()
{
return config('app.account_realm');
return $this->space->account_realm;
}
public function getResolvedRealmAttribute()
{
return config('app.account_realm') ?? $this->domain;
return $this->space->account_realm ?? $this->domain;
}
public function getConfirmationKeyExpiresAttribute()
@ -319,11 +386,6 @@ class Account extends Authenticatable
return null;
}
public function getSha256PasswordAttribute()
{
return $this->passwords()->where('algorithm', 'SHA-256')->exists();
}
public static function dtmfProtocolsRule()
{
return implode(',', array_keys(self::$dtmfProtocols));
@ -336,18 +398,42 @@ class Account extends Authenticatable
public function getSuperAdminAttribute(): bool
{
return SipDomain::where('domain', $this->domain)->where('super', true)->exists() && $this->admin;
return Space::where('domain', $this->domain)->where('super', true)->exists() && $this->admin;
}
/**
* Provisioning
*/
public function getProvisioningUrlAttribute(): string
{
return replaceHost(
route('provisioning.provision', $this->getProvisioningTokenAttribute()),
$this->space->host
);
}
public function getProvisioningQrcodeUrlAttribute(): string
{
return replaceHost(
route('provisioning.qrcode', $this->getProvisioningTokenAttribute()),
$this->space->host
);
}
public function getProvisioningWizardUrlAttribute(): string
{
return replaceHost(
route('provisioning.wizard', $this->getProvisioningTokenAttribute()),
$this->space->host
);
}
/**
* Utils
*/
public function activationExpired(): bool
{
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function generateApiKey(?Request $request = null): ApiKey
public function generateUserApiKey(?string $ip = null): ApiKey
{
$this->apiKey()->delete();
@ -355,7 +441,7 @@ class Account extends Authenticatable
$apiKey->account_id = $this->id;
$apiKey->last_used_at = Carbon::now();
$apiKey->key = Str::random(40);
$apiKey->ip = $request ? $request->ip() : '127.0.0.1';
$apiKey->ip = $ip;
$apiKey->save();
return $apiKey;
@ -379,10 +465,12 @@ class Account extends Authenticatable
return $authToken;
}
public function recover(?string $code = null): string
public function recover(?string $code = null, ?string $phone = null, ?string $email = null): string
{
$recoveryCode = new RecoveryCode;
$recoveryCode->code = $code ?? generatePin();
$recoveryCode->phone = $phone;
$recoveryCode->email = $email;
$recoveryCode->account_id = $this->id;
if (request()) {

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

@ -19,6 +19,7 @@
namespace App;
use Illuminate\Validation\Rule;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AccountCreationToken extends Consommable

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

@ -0,0 +1,42 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AccountRecoveryToken extends Consommable
{
use HasFactory;
protected $hidden = ['id', 'updated_at', 'created_at'];
protected $appends = ['expire_at'];
protected ?string $configExpirationMinutesKey = 'account_recovery_token_expiration_minutes';
public function account()
{
return $this->belongsTo(Account::class);
}
public function consume()
{
$this->used = true;
$this->save();
}
public function consumed(): bool
{
return $this->used == true;
}
public function toLog()
{
return [
'token' => $this->token,
'pn_param' => $this->pn_param,
'used' => $this->used,
'account_id' => $this->account_id,
'ip' => $this->ip,
'user_agent' => $this->user_agent,
];
}
}

View file

@ -28,8 +28,12 @@ class ApiKey extends Model
protected $table = 'api_keys';
protected $casts = [
'last_used_at' => 'datetime',
];
public function account()
{
return $this->belongsTo(Account::class);
return $this->belongsTo(Account::class)->withoutGlobalScopes();
}
}

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(
@ -46,10 +41,10 @@ class ClearAccountsTombstones extends Command
$this->info($tombstones->count() . ' tombstones deleted');
$tombstones->delete();
return 0;
return Command::SUCCESS;
}
$this->info($tombstones->count() . ' tombstones to delete');
return 0;
return Command::SUCCESS;
}
}

View file

@ -27,30 +27,42 @@ use App\ApiKey;
class ClearApiKeys extends Command
{
protected $signature = 'accounts:clear-api-keys {minutes?}';
protected $description = 'Clear the expired API Keys after n minutes';
public function __construct()
{
parent::__construct();
}
protected $description = 'Clear the expired user API Keys after n minutes and clear the other expired admin keys';
public function handle()
{
// User API Keys
$minutes = $this->argument('minutes') ?? config('app.api_key_expiration_minutes');
if ($minutes == 0) {
$this->info('Expiration time is set to 0, nothing to clear');
return 0;
return Command::SUCCESS;
}
$this->info('Deleting api keys unused after ' . $minutes . ' minutes');
$this->info('Deleting user API Keys unused after ' . $minutes . ' minutes');
$count = ApiKey::where(
'last_used_at',
'<',
Carbon::now()->subMinutes($minutes)->toDateTimeString()
)->delete();
$count = ApiKey::whereNull('expires_after_last_used_minutes')
->where('last_used_at', '<', Carbon::now()->subMinutes($minutes)->toDateTimeString())
->delete();
$this->info($count . ' api keys deleted');
$this->info($count . ' user API Keys deleted');
// Admin API Keys
$keys = ApiKey::whereNotNull('expires_after_last_used_minutes')
->where('expires_after_last_used_minutes', '>', 0)
->with('account')
->get();
$count = 0;
foreach ($keys as $key) {
if ($key->last_used_at->addMinutes($key->expires_after_last_used_minutes)->isPast()) {
$this->info('Deleting ' . $key->account->identifier . ' admin API Key expired after ' . $key->expires_after_last_used_minutes .'min');
$key->delete();
$count++;
}
}
$this->info($count . ' admin API Keys deleted');
}
}

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(
@ -53,10 +48,10 @@ class ClearUnconfirmed extends Command
$accounts->delete();
$this->info($count . ' accounts deleted');
return 0;
return Command::SUCCESS;
}
$this->info($count . ' accounts to delete');
return 0;
return Command::SUCCESS;
}
}

View file

@ -23,12 +23,12 @@ use Illuminate\Console\Command;
use Carbon\Carbon;
use App\Account;
use App\SipDomain;
use App\Space;
class CreateAdminAccount extends Command
{
protected $signature = 'accounts:create-admin-account {--u|username=} {--p|password=} {--d|domain=}';
protected $description = 'Create an admin account';
protected $signature = 'accounts:create-admin-account {--u|username=} {--p|password=} {--d|domain=} {--k|api_key_ip=}';
protected $description = 'Create an admin account and generate an API Key';
public function __construct()
{
@ -37,9 +37,9 @@ class CreateAdminAccount extends Command
public function handle()
{
$sipDomains = SipDomain::all('domain')->pluck('domain');
$spaces = Space::all('domain')->pluck('domain');
$this->info('Your will create a new admin account in the database, existing accounts with the same credentials will be overwritten');
$this->info('Your creating a new admin account in the database, existing accounts with the same credentials will be overwritten');
$username = $this->option('username');
$domain = $this->option('domain');
@ -50,7 +50,7 @@ class CreateAdminAccount extends Command
}
if (!$this->option('domain')) {
$domain = $this->ask('What will be the admin domain? Default: ' . $sipDomains->first());
$domain = $this->ask('What will be the admin domain? Default: ' . $spaces->first());
}
if (!$this->option('password')) {
@ -58,11 +58,11 @@ class CreateAdminAccount extends Command
}
$username = $username ?? 'admin';
$domain = $domain ?? $sipDomains->first();
$domain = $domain ?? $spaces->first();
$password = $password ?? 'change_me';
if (!$sipDomains->contains($domain)) {
$this->error('The domain must be one of the following ones: ' . $sipDomains->implode(', '));
if (!$spaces->contains($domain)) {
$this->error('The domain must be one of the following ones: ' . $spaces->implode(', '));
$this->comment('You can create an extra domain using the dedicated console command');
return Command::FAILURE;
}
@ -80,7 +80,6 @@ class CreateAdminAccount extends Command
$account = new Account;
$account->username = $username;
$account->domain = $domain;
$account->email = 'admin_test@sip.example.org';
$account->activated = true;
$account->user_agent = 'Test';
$account->ip_address = '0.0.0.0';
@ -90,11 +89,11 @@ class CreateAdminAccount extends Command
$account->created_at = Carbon::now()->subYears(3);
$account->save();
$account->generateApiKey();
$account->generateUserApiKey(ip: $this->option('api_key_ip') ?? null);
$account->updatePassword($password);
$this->info('Admin test account created: "' . $username . '@' . $domain . '" | Password: "' . $password . '" | API Key: "' . $account->apiKey->key . '"');
$this->info('Admin test account created: "' . $username . '@' . $domain . '" | Password: "' . $password . '" | API Key: "' . $account->apiKey->key . '" (valid on ' . ($account->apiKey->ip ?? 'any') . ' ip)');
return 0;
return Command::SUCCESS;
}
}

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('sip_domains:create-update', [
'domain' => $domain,
$this->call('spaces:create-update', [
'sip_domain' => $domain,
'host' => $domain,
'name' => $domain,
'--super' => 'true'
]);
@ -68,6 +65,6 @@ class CreateAdminTest extends Command
$this->info('API Key updated to: ' . $secret);
return 0;
return Command::SUCCESS;
}
}

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,23 +28,18 @@ 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();
if (!$account) {
$this->error('Account not found, please use an existing account id');
return 1;
return Command::FAILURE;
}
if ($account->admin) {
$this->error('The account is already having the admin role');
return 1;
return Command::FAILURE;
}
$account->admin = true;
@ -52,6 +47,6 @@ class SetAdmin extends Command
$this->info('Account '.$account->identifier.' is now admin');
return 0;
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use App\StatisticsCall;
use App\StatisticsMessage;
use Carbon\Carbon;
use Illuminate\Console\Command;
class ClearStatistics extends Command
{
protected $signature = 'app:clear-statistics {days} {--apply}';
protected $description = 'Command description';
public function handle()
{
$calls = StatisticsCall::where(
'created_at',
'<',
Carbon::now()->subDays($this->argument('days'))->toDateTimeString()
);
$messages = StatisticsMessage::where(
'created_at',
'<',
Carbon::now()->subDays($this->argument('days'))->toDateTimeString()
);
$callsCount = $calls->count();
$messagesCount = $messages->count();
if ($this->option('apply')) {
$this->info($callsCount . ' calls statistics in deletion…');
$calls->delete();
$this->info($callsCount . ' calls statistics deleted');
$this->info($messagesCount . ' messages statistics in deletion…');
$messages->delete();
$this->info($messagesCount . ' messages statistics deleted');
return Command::SUCCESS;
}
$this->info($callsCount . ' calls statistics to delete');
$this->info($messagesCount . ' messages statistics to delete');
return Command::SUCCESS;
}
}

View file

@ -1,50 +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\Console\Commands\SipDomains;
use App\SipDomain;
use Illuminate\Console\Command;
class CreateUpdate extends Command
{
protected $signature = 'sip_domains:create-update {domain} {--super}';
protected $description = 'Create a SIP Domain';
public function handle()
{
$this->info('Your will create or update a SIP Domain in the database');
$sipDomain = SipDomain::where('domain', $this->argument('domain'))->firstOrNew();
$sipDomain->domain = $this->argument('domain');
$sipDomain->exists
? $this->info('The domain already exists, updating it')
: $this->info('A new domain will be created');
$sipDomain->super = (bool)$this->option('super');
$sipDomain->super
? $this->info('Set as a super domain')
: $this->info('Set as a normal domain');
$sipDomain->save();
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,67 @@
<?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\Console\Commands\Spaces;
use App\Space;
use Illuminate\Console\Command;
class CreateUpdate extends Command
{
protected $signature = 'spaces:create-update {sip_domain} {host} {name} {--super}';
protected $description = 'Create a Space';
public function handle()
{
$this->info('Your are creating or updating a Space in the database');
if (empty(config('app.root_host'))) {
$this->error('The environnement variable APP_ROOT_HOST doesn\'t seems to be set');
}
if (!str_ends_with($this->argument('host'), config('app.root_host'))) {
$this->error('The provided host doesn\'t seems to ends with ' . config('app.root_host'));
}
$space = Space::where('domain', $this->argument('sip_domain'))->firstOrNew();
$space->host = $this->argument('host');
$space->domain = $this->argument('sip_domain');
$space->name = $this->argument('name');
if ($hostSpace = Space::where('host', $this->argument('host'))->first()) {
if (!$space->exists && $hostSpace->domain != $space->domain) {
$this->error('A Space with this host and a different sip_domain already exists in the database');
return Command::FAILURE;
}
}
$space->exists
? $this->info('The space already exists, updating it')
: $this->info('A new Space will be created');
$space->super = (bool)$this->option('super');
$space->super
? $this->info('Set as a super Space')
: $this->info('Set as a normal Space');
$space->save();
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands\Spaces;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
use App\Mail\ExpiringSpace;
use App\Space;
class ExpirationEmails extends Command
{
protected $signature = 'spaces:expiration-emails {days?}';
protected $description = 'Send an expiration email on the designated configured days before expiration. Days must be ordered descending and comma separated (eg. 7,3,1)';
public function handle()
{
$days = ['7','3','1'];
if ($this->argument('days')) {
preg_match_all('/\d++/', $this->argument('days'), $matches);
if (!empty($matches[0])) {
$i = 0;
while ($i + 1 < count($matches[0]) && (int)$matches[0][$i] > (int)$matches[0][$i + 1]) {
$i++;
}
if ($i != count($matches[0]) - 1) {
$this->error('The days must be integer, ordered descending and comma separated');
return Command::FAILURE;
}
$days = $matches[0];
}
}
$expiringSpaces = Space::whereNotNull('expire_at')->whereDate('expire_at', '>=', Carbon::now())->get();
foreach ($expiringSpaces as $expiringSpace) {
if (in_array($expiringSpace->daysLeft, $days)) {
$this->info($expiringSpace->name . ' (' . $expiringSpace->host . ') is expiring in ' . $expiringSpace->daysLeft . ' days');
$admins = $expiringSpace->admins()->withoutGlobalScopes()->whereNotNull('email')->get();
$this->info('Sending an email to the admins ' . $admins->implode('email', ','));
foreach ($admins as $admin) {
Mail::to($admin->email)->send(new ExpiringSpace($expiringSpace));
}
}
}
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands\Spaces;
use Illuminate\Console\Command;
use App\Space;
class ImportConfigurationFromDotEnv extends Command
{
protected $signature = 'spaces:import-configuration-from-dot-env {sip_domain}';
protected $description = 'Import the deprecated space DotEnv configuration in a Space';
public function handle()
{
$space = Space::where('domain', $this->argument('sip_domain'))->first();
if (!$space) {
$this->error('The space cannot be found');
return Command::SUCCESS;
}
$this->info('The following configuration will be imported in the space ' . $space->domain);
$this->info('The existing settings will be overwritten:');
$space->name = env('APP_NAME', null);
$space->custom_theme = env('INSTANCE_CUSTOM_THEME', false);
$space->web_panel = env('WEB_PANEL', true);
$space->copyright_text = env('INSTANCE_COPYRIGHT', null);
$space->intro_registration_text = env('INSTANCE_INTRO_REGISTRATION', null);
$space->newsletter_registration_address = env('NEWSLETTER_REGISTRATION_ADDRESS', null);
$space->account_proxy_registrar_address = env('ACCOUNT_PROXY_REGISTRAR_ADDRESS', 'sip.domain.com');
$space->account_realm = env('ACCOUNT_REALM', null);
$space->custom_provisioning_overwrite_all = env('ACCOUNT_PROVISIONING_OVERWRITE_ALL', false);
$space->provisioning_use_linphone_provisioning_header = env('ACCOUNT_PROVISIONING_USE_X_LINPHONE_PROVISIONING_HEADER', true);
$space->public_registration = env('PUBLIC_REGISTRATION', true);
$space->phone_registration = env('PHONE_AUTHENTICATION', true);
$space->intercom_features = env('INTERCOM_FEATURES', false);
foreach ($space->getDirty() as $key => $value) {
$show = ' - ' . $key . ' => ';
$show .= ($value == null) ? 'null' : $value;
$this->info($show);
}
if ($this->confirm('Do you want to update ' . $space->domain . '?', false)) {
$space->save();
$this->info('Space updated');
}
}
}

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

@ -27,6 +27,11 @@ abstract class Consommable extends Model
$this->user_agent = $request->userAgent();
}
public function offed(): bool
{
return $this->consumed() || $this->expired();
}
public function consumed(): bool
{
return $this->{$this->consommableAttribute} == null;
@ -35,7 +40,7 @@ abstract class Consommable extends Model
public function getExpireAtAttribute(): ?string
{
if ($this->isExpirable()) {
return $this->created_at->addMinutes(config('app.' . $this->configExpirationMinutesKey))->toJSON();
return $this->created_at->addMinutes((int)config('app.' . $this->configExpirationMinutesKey))->toJSON();
}
return null;
@ -44,13 +49,13 @@ abstract class Consommable extends Model
public function expired(): bool
{
return ($this->isExpirable()
&& Carbon::now()->subMinutes(config('app.' . $this->configExpirationMinutesKey))->isAfter($this->created_at));
&& Carbon::now()->subMinutes((int)config('app.' . $this->configExpirationMinutesKey))->isAfter($this->created_at));
}
private function isExpirable(): bool
{
return $this->configExpirationMinutesKey != null
&& config('app.' . $this->configExpirationMinutesKey) != null
&& config('app.' . $this->configExpirationMinutesKey) > 0;
&& (int)config('app.' . $this->configExpirationMinutesKey) > 0;
}
}

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

@ -16,29 +16,24 @@
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;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ActivationExpiration extends Model
class ExternalAccount extends Model
{
use HasFactory;
protected $casts = [
'expires' => 'datetime:Y-m-d H:i:s',
];
public const PROTOCOLS = ['UDP', 'TCP','TLS'];
public function account()
{
return $this->belongsTo(Account::class);
}
public function isExpired()
public function getIdentifierAttribute(): string
{
$now = Carbon::now();
return $this->expires->lessThan($now);
return $this->attributes['username'] . '@' . $this->attributes['domain'];
}
}

View file

@ -17,9 +17,11 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use App\Account;
use App\Space;
use App\DigestNonce;
use Illuminate\Http\Request;
use League\CommonMark\CommonMarkConverter;
@ -27,10 +29,15 @@ use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use Illuminate\Support\Facades\DB;
function space(): ?Space
{
return is_object(request()->space) ? request()->space : null;
}
function passwordAlgorithms(): array
{
return [
'MD5' => 'md5',
'MD5' => 'md5',
'SHA-256' => 'sha256',
];
}
@ -40,9 +47,11 @@ function generateNonce(): string
return Str::random(32);
}
function getRequestBoolean(Request $request, string $key): bool
function getRequestBoolean(Request $request, string $key, bool $reversed = false): bool
{
return $request->has($key) ? $request->get($key) == "true" : false;
$bool = $request->has($key) ? $request->get($key) == "on" : false;
return $reversed ? !$bool : $bool;
}
function generateValidNonce(Account $account): string
@ -92,8 +101,8 @@ function markdownDocumentationView(string $view): string
$converter->getEnvironment()->addExtension(new TableOfContentsExtension());
return (string) $converter->convert(
(string)view($view, [
'app_name' => config('app.name')
(string) view($view, [
'app_name' => space()->name
])->render()
);
}
@ -138,13 +147,27 @@ function isRegularExpression(string $string): bool
return $isRegularExpression;
}
function replaceHost(string $url, string $host): string
{
$components = parse_url($url);
return str_replace($components['host'], $host, $url);
}
function resolveDomain(Request $request): string
{
return $request->has('domain')
&& $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
@ -156,7 +179,7 @@ function resolveUserContacts(Request $request)
{
$selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol', 'display_name'];
return Account::whereIn('id', function ($query) use ($request) {
return Account::withoutGlobalScopes()->whereIn('id', function ($query) use ($request) {
$query->select('contact_id')
->from('contacts')
->where('account_id', $request->user()->id)
@ -191,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);
}
/**
@ -203,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

@ -28,13 +28,6 @@ use App\Services\AccountService;
class AccountController extends Controller
{
public function documentation(Request $request)
{
return view('account.documentation', [
'documentation' => markdownDocumentationView('account.documentation_markdown')
]);
}
public function blocked(Request $request)
{
return view('account.blocked');

View file

@ -34,7 +34,7 @@ class ApiKeyController extends Controller
public function update(Request $request)
{
$account = $request->user();
$account->generateApiKey($request);
$account->generateUserApiKey($request->ip());
return redirect()->back();
}

View file

@ -72,7 +72,7 @@ class AuthTokenController extends Controller
$authToken->delete();
return redirect()->route('account.dashboard');
return redirect()->route('account.home');
}
/**
@ -87,6 +87,6 @@ class AuthTokenController extends Controller
$authToken->save();
}
return redirect()->route('account.dashboard');
return redirect()->route('account.home');
}
}

View file

@ -33,13 +33,19 @@ class AuthenticateController extends Controller
public function login(Request $request)
{
if (Auth::user()) {
if ($request->user()) {
if ($request->user()->superAdmin) {
return redirect()->route('admin.spaces.index');
} elseif ($request->user()->admin) {
return redirect()->route('admin.spaces.me');
}
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)
@ -57,7 +63,7 @@ class AuthenticateController extends Controller
}
if (!$account) {
return redirect()->back()->withErrors(['authentication' => 'Wrong username or password']);
return redirect()->back()->withErrors(['authentication' => __('Incorrect username or password')]);
}
// Try out the passwords
@ -67,34 +73,11 @@ class AuthenticateController extends Controller
bchash($account->username, $account->resolvedRealm, $request->get('password'), $password->algorithm)
)) {
Auth::login($account);
return redirect()->route('account.dashboard');
return redirect()->route('account.home');
}
}
return redirect()->back()->withErrors(['authentication' => 'Wrong username or password']);
}
/**
* Deprecated
*/
public function validateEmail(Request $request, string $code)
{
$request->merge(['code' => $code]);
$request->validate(['code' => 'required|size:' . self::$emailCodeSize]);
$account = Account::where('confirmation_key', $code)->first();
if (!$account) {
return redirect()->route('account.login');
}
$account->confirmation_key = null;
$account->activated = true;
$account->save();
Auth::login($account);
return redirect()->route('account.dashboard');
return redirect()->back()->withErrors(['authentication' => __('Incorrect username or password')]);
}
public function loginAuthToken(Request $request, ?string $token = null)
@ -120,7 +103,7 @@ class AuthenticateController extends Controller
$authToken->delete();
return redirect()->route('account.dashboard');
return redirect()->route('account.home');
}
return view('account.authenticate.auth_token', [

View file

@ -21,13 +21,13 @@ namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Libraries\FlexisipConnector;
use App\Libraries\FlexisipRedisConnector;
class DeviceController extends Controller
{
public function index(Request $request)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
return view(
'account.device.index',
@ -40,7 +40,7 @@ class DeviceController extends Controller
public function delete(Request $request, string $uuid)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
return view(
'account.device.delete',
@ -54,7 +54,7 @@ class DeviceController extends Controller
public function destroy(Request $request)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
$connector->deleteDevice($request->user()->identifier, $request->get('uuid'));
return redirect()->route('account.device.index');

View file

@ -60,7 +60,7 @@ class EmailController extends Controller
}
return redirect()->route('account.email.change')->withErrors([
'code' => 'The code entered was not valid'
'code' => __('The code is not valid')
]);
}
}

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,11 +21,9 @@ namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller
{
public function show(Request $request)
@ -49,16 +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]);
if (!empty($account->email)) {
Mail::to($account)->send(new ConfirmedRegistration($account));
}
return redirect()->route('account.dashboard');
return redirect()->route('account.logout');
}
}

View file

@ -60,7 +60,7 @@ class PhoneController extends Controller
}
return redirect()->route('account.phone.change')->withErrors([
'code' => 'The code entered was not valid'
'code' => __('The code is not valid')
]);
}
}

View file

@ -40,6 +40,13 @@ class ProvisioningController extends Controller
]);
}
public function wizard(Request $request, string $provisioningToken)
{
return view('provisioning.wizard', [
'token' => $provisioningToken
]);
}
public function qrcode(Request $request, string $provisioningToken)
{
$account = Account::withoutGlobalScopes()
@ -51,10 +58,6 @@ class ProvisioningController extends Controller
})
->firstOrFail();
if ($account->activationExpired()) {
abort(404);
}
$params = ['provisioning_token' => $provisioningToken];
if ($request->has('reset_password')) {
@ -130,7 +133,7 @@ class ProvisioningController extends Controller
})
->firstOrFail();
if ($account->activationExpired() || ($provisioningToken != $account->provisioning_token)) {
if ($provisioningToken != $account->provisioning_token) {
return abort(404);
}
@ -148,7 +151,7 @@ class ProvisioningController extends Controller
private function checkProvisioningHeader(Request $request)
{
if (!$request->hasHeader('x-linphone-provisioning')
&& config('app.provisioning_use_x_linphone_provisioning_header')) {
&& $request->space->provisioning_use_linphone_provisioning_header) {
abort(400, 'x-linphone-provisioning header is missing');
}
}
@ -169,11 +172,8 @@ class ProvisioningController extends Controller
$dom->appendChild($config);
// Default RC file handling
$rcFile = config('app.provisioning_rc_file');
if (file_exists($rcFile)) {
$rc = parse_ini_file($rcFile, 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));
@ -206,7 +244,7 @@ class ProvisioningController extends Controller
if ($account) {
$ui = $xpath->query("//section[@name='ui']")->item(0);
if ($ui == null && $account->sipDomain) {
if ($ui == null && $account->space) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'ui');
@ -225,7 +263,7 @@ class ProvisioningController extends Controller
'max_account',
] as $key) {
// Cast the booleans into integers
$entry = $dom->createElement('entry', (int)$account->sipDomain->$key);
$entry = $dom->createElement('entry', (int)$account->space->$key);
$entry->setAttribute('name', $key);
$section->appendChild($entry);
}
@ -251,66 +289,6 @@ class ProvisioningController extends Controller
}
$passwords = $account->passwords()->get();
$authInfoIndex = 0;
// CoTURN
if (hasCoTURNConfigured()) {
list($username, $password) = array_values(getCoTURNCredentials());
// net
$section = $xpath->query("//section[@name='net']")->item(0);
if ($section == null) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'net');
$config->appendChild($section);
}
$ref = Str::random(8);
$entry = $dom->createElement('entry', $ref);
$entry->setAttribute('name', 'nat_policy_ref');
$section->appendChild($entry);
// nat_policy_0
$section = $dom->createElement('section');
$section->setAttribute('name', 'nat_policy_0');
$config->appendChild($section);
$entry = $dom->createElement('entry', $ref);
$entry->setAttribute('name', 'ref');
$section->appendChild($entry);
$entry = $dom->createElement('entry', config('app.coturn_server_host'));
$entry->setAttribute('name', 'stun_server');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $username);
$entry->setAttribute('name', 'stun_server_username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'turn,ice');
$entry->setAttribute('name', 'protocols');
$section->appendChild($entry);
// auth_info_x
$section = $xpath->query("//section[@name='auth_info_" . $authInfoIndex . "']")->item(0);
if ($section == null) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$config->appendChild($section);
$authInfoIndex++;
}
$entry = $dom->createElement('entry', $username);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $password);
$entry->setAttribute('name', 'passwd');
$section->appendChild($entry);
}
foreach ($passwords as $password) {
$section = $xpath->query("//section[@name='auth_info_" . $authInfoIndex . "']")->item(0);
@ -356,7 +334,7 @@ class ProvisioningController extends Controller
}
// Overwrite all the entries
if (config('app.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

@ -20,12 +20,16 @@
namespace App\Http\Controllers\Account;
use App\Account;
use App\AccountRecoveryToken;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\AccountService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class RecoveryController extends Controller
{
public function showEmail(Request $request)
@ -36,10 +40,16 @@ class RecoveryController extends Controller
]);
}
public function showPhone(Request $request)
public function showPhone(Request $request, string $accountRecoveryToken)
{
$accountRecoveryToken = AccountRecoveryToken::where('token', $accountRecoveryToken)
->where('used', false)
->firstOrFail();
return view('account.recovery.show', [
'method' => 'phone',
'account_recovery_token' => $accountRecoveryToken->token,
'phone' => $request->get('phone'),
'domain' => resolveDomain($request)
]);
}
@ -49,7 +59,8 @@ class RecoveryController extends Controller
$rules = [
'email' => 'required_without:phone|email|exists:accounts,email',
'phone' => 'required_without:email|starts_with:+',
'h-captcha-response' => captchaConfigured() ? 'required|HCaptcha' : '',
'h-captcha-response' => captchaConfigured() ? 'required_with:email|HCaptcha' : '',
'account_recovery_token' => 'required_with:phone',
];
$account = null;
@ -86,17 +97,25 @@ class RecoveryController extends Controller
}
if (!$account) {
return redirect()->back()->withErrors(['identifier' => 'The account doesn\'t exists']);
return redirect()->back()->withErrors(['identifier' => __("The account doesn't exists")]);
}
if ($account->failedRecentRecovery()) {
return redirect()->back()->withErrors(['code' => 'Account recovered recently, try again later']);
return redirect()->back()->withErrors(['code' => __('Account recovered recently, try again later')]);
}
if ($request->get('email')) {
$account = (new AccountService)->recoverByEmail($account);
$account = (new AccountService)->recoverByEmail($account, $request->get('email'));
} elseif ($request->get('phone')) {
$account = (new AccountService)->recoverByPhone($account);
$accountRecoveryToken = AccountRecoveryToken::where('token', $request->get('account_recovery_token'))
->where('used', false)
->first();
if (!$accountRecoveryToken) {
abort(403, 'Wrong Account Recovery Token');
}
$account = (new AccountService)->recoverByPhone($account, $request->get('phone'), $accountRecoveryToken);
}
return view('account.recovery.confirm', [
@ -124,7 +143,7 @@ class RecoveryController extends Controller
return redirect()->route($request->get('method') == 'phone'
? 'account.recovery.show.phone'
: 'account.recovery.show.email')->withErrors([
'code' => 'The code is expired'
'code' => __('The code has expired')
]);
}
@ -132,7 +151,7 @@ class RecoveryController extends Controller
return redirect()->route($request->get('method') == 'phone'
? 'account.recovery.show.phone'
: 'account.recovery.show.email')->withErrors([
'code' => 'The code entered was not valid, try again later'
'code' => 'The code is not valid'
]);
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\Account;
use App\ResetPasswordEmailToken;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ResetPasswordEmailController extends Controller
{
public function change(string $token)
{
$token = ResetPasswordEmailToken::where('token', $token)->firstOrFail();
return view('account.password_reset', [
'token' => $token
]);
}
public function reset(Request $request)
{
$request->validate([
'token' => 'required|size:16',
'password' => 'required|min:8|confirmed',
'h-captcha-response' => captchaConfigured() ? 'required|HCaptcha' : ''
]);
$token = ResetPasswordEmailToken::where('token', $request->get('token'))->firstOrFail();
if ($token->offed()) abort(403);
$token->account->updatePassword($request->get('password'));
$token->consume();
return view('account.password_changed');
}
}

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)
{
@ -55,7 +55,7 @@ class AccountAccountTypeController extends Controller
Log::channel('events')->info('Web Admin: Account type attached', ['id' => $account->identifier, 'type_id' => $request->get('account_type_id')]);
return redirect()->route('admin.account.edit', $account);
return redirect()->route('admin.account.show', $account)->withFragment('#types');
}
public function destroy(Request $request, int $id, int $typeId)
@ -66,6 +66,6 @@ class AccountAccountTypeController extends Controller
Log::channel('events')->info('Web Admin: Account type detached', ['id' => $account->identifier, 'type_id' => $request->get('account_type_id')]);
return redirect()->route('admin.account.edit', $account);
return redirect()->route('admin.account.show', $account)->withFragment('#types');
}
}

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)
{
@ -56,7 +56,7 @@ class AccountActionController extends Controller
Log::channel('events')->info('Web Admin: Account action created', ['id' => $account->identifier, 'action' => $accountAction->key]);
return redirect()->route('admin.account.edit', $accountAction->account);
return redirect()->route('admin.account.show', $accountAction->account)->withFragment('#actions');
}
public function edit(int $accountId, int $actionId)
@ -91,7 +91,7 @@ class AccountActionController extends Controller
Log::channel('events')->info('Web Admin: Account action updated', ['id' => $account->identifier, 'action' => $accountAction->key]);
return redirect()->route('admin.account.edit', $account);
return redirect()->route('admin.account.show', $account)->withFragment('#actions');
}
public function delete(int $accountId, int $actionId)
@ -99,6 +99,7 @@ class AccountActionController extends Controller
$account = Account::findOrFail($accountId);
return view('admin.account.action.delete', [
'account' => $account,
'action' => $account->actions()
->where('id', $actionId)
->firstOrFail()
@ -116,6 +117,6 @@ class AccountActionController extends Controller
Log::channel('events')->info('Web Admin: Account action deleted', ['id' => $accountAction->account->identifier, 'action_id' => $accountAction->key]);
return redirect()->route('admin.account.edit', $accountAction->account);
return redirect()->route('admin.account.show', $accountAction->account)->withFragment('#actions');
}
}

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,16 +17,31 @@
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;
use Illuminate\Support\Facades\Log;
use App\Account;
use App\ContactsList;
class AccountContactController extends Controller
class ContactController extends Controller
{
public function index(int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.contact.index', [
'account' => $account,
'contacts_lists' => ContactsList::whereNotIn('id', function ($query) use ($accountId) {
$query->select('contacts_list_id')
->from('account_contacts_list')
->where('account_id', $accountId);
})->withCount('contacts')->get()
]);
}
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
@ -47,7 +62,7 @@ class AccountContactController extends Controller
if (!$contact) {
return redirect()->back()->withErrors([
'sip' => 'The contact SIP address doesn\'t exists'
'sip' => __("The contact doesn't exists")
]);
}
@ -56,7 +71,7 @@ class AccountContactController extends Controller
Log::channel('events')->info('Web Admin: Account contact added', ['id' => $account->identifier, 'contact' => $contact->identifier]);
return redirect()->route('admin.account.edit', $account);
return redirect()->route('admin.account.contact.index', $account);
}
public function delete(int $accountId, int $contactId)
@ -79,6 +94,6 @@ class AccountContactController extends Controller
Log::channel('events')->info('Web Admin: Account contact removed', ['id' => $account->identifier, 'contact' => $contact->identifier]);
return redirect()->route('admin.account.edit', $account);
return redirect()->route('admin.account.contact.index', $account);
}
}

View file

@ -17,18 +17,18 @@
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\FlexisipConnector;
use App\Libraries\FlexisipRedisConnector;
class AccountDeviceController extends Controller
class DeviceController extends Controller
{
public function index(int $accountId)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
$account = Account::findOrFail($accountId);
return view(
@ -42,14 +42,14 @@ class AccountDeviceController extends Controller
public function delete(int $accountId, string $uuid)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
$account = Account::findOrFail($accountId);
return view(
'admin.account.device.delete',
[
'account' => $account,
'device' => $connector->getDevices($account->identifier)
'device' => $connector->getDevices($account->identifier)
->where('uuid', $uuid)->first()
]
);
@ -57,11 +57,11 @@ class AccountDeviceController extends Controller
public function destroy(Request $request, int $accountId)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
$account = Account::findOrFail($accountId);
$connector->deleteDevice($account->identifier, $request->get('uuid'));
return redirect()->route('admin.account.device.index', $account);
return redirect()->route('admin.account.show', $account);
}
}

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,18 +25,8 @@ use Illuminate\Http\Request;
use App\Account;
use App\AccountDictionaryEntry;
class AccountDictionaryController extends Controller
class DictionaryController extends Controller
{
public function index(int $accountId)
{
return view(
'admin.account.dictionary.index',
[
'account' => Account::findOrFail($accountId)
]
);
}
public function create(int $accountId)
{
return view('admin.account.dictionary.create_edit', [
@ -61,7 +51,7 @@ class AccountDictionaryController extends Controller
accountServiceAccountEditedHook($request, $account);
}
return redirect()->route('admin.account.dictionary.index', $account->id);
return redirect()->route('admin.account.show', $account);
}
public function edit(int $accountId, string $key)
@ -91,7 +81,7 @@ class AccountDictionaryController extends Controller
accountServiceAccountEditedHook($request, $account);
}
return redirect()->route('admin.account.dictionary.index', $account->id);
return redirect()->route('admin.account.show', $account);
}
public function delete(int $accountId, string $key)
@ -102,7 +92,7 @@ class AccountDictionaryController extends Controller
'admin.account.dictionary.delete',
[
'account' => $account,
'entry' => $account->dictionaryEntries()->where('key', $key)->firstOrFail()
'entry' => $account->dictionaryEntries()->where('key', $key)->firstOrFail()
]
);
}
@ -112,6 +102,6 @@ class AccountDictionaryController extends Controller
$account = Account::findOrFail($accountId);
$account->dictionaryEntries()->where('key', $request->get('key'))->delete();
return redirect()->route('admin.account.dictionary.index', $account);
return redirect()->route('admin.account.show', $account);
}
}

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

@ -0,0 +1,388 @@
<?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\ExternalAccount;
use App\Password;
use App\PhoneCountry;
use App\Space;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
use Propaganistas\LaravelPhone\PhoneNumber;
class ImportController extends Controller
{
private Collection $errors;
private string $importDirectory = 'imported_csv';
public function __construct()
{
$this->errors = collect();
}
public function create(Request $request)
{
return view('admin.account.import.create', [
'domains' => $request->user()->superAdmin
? Space::pluck('domain')
: [$request->user()->domain]
]);
}
public function store(Request $request)
{
$request->validate([
'csv' => ['required', File::types(['csv', 'txt'])],
'domain' => 'required|exists:spaces,domain'
]);
$domain = $request->user()->superAdmin
? $request->get('domain')
: $request->user()->domain;
/**
* General formating checking
*/
$csv = fopen($request->file('csv'), 'r');
$line = fgets($csv);
fclose($csv);
$lines = collect();
$this->errors['Wrong file format'] = "The number of columns doesn't matches the reference file. The first MUST be the same as the reference file";
$firstLine = 'Username,Password,Role,Status,Phone,Email,External Username,External Domain,External Password,External Realm, External Registrar,External Outbound Proxy,External Protocol';
if (substr($line, 0, strlen($firstLine)) == $firstLine) {
$lines = $this->csvToCollection($request->file('csv'));
unset($this->errors['Wrong file format']);
}
/**
* Error checking
*/
// Usernames
$existingUsernames = Account::where('domain', $domain)
->whereIn('username', $lines->pluck('username')->all())
->pluck('username');
if ($existingUsernames->isNotEmpty()) {
$this->errors['Those usernames already exists'] = $existingUsernames->join(', ', ' and ');
}
if ($duplicates = $lines->pluck('username')->duplicates()) {
if ($duplicates->isNotEmpty()) {
$this->errors['Those usernames are declared several times'] = $duplicates->join(', ', ' and ');
}
}
if ($lines->pluck('username')->contains(function ($value) {
return strlen($value) < 2;
})) {
$this->errors['Some usernames are shorter than expected'] = '';
}
// Passwords
if ($lines->pluck('password')->contains(function ($value) {
return strlen($value) < 6;
})) {
$this->errors['Some passwords are shorter than expected'] = '';
}
// Roles
if ($lines->pluck('role')->contains(function ($value) {
return !in_array($value, ['admin', 'user']);
})) {
$this->errors['Some roles are not correct'] = '';
}
// Status
if ($lines->pluck('status')->contains(function ($value) {
return !in_array($value, ['active', 'inactive']);
})) {
$this->errors['Some statuses are not correct'] = '';
}
// Phones
$phoneCountries = PhoneCountry::where('activated', true)->get();
if ($phones = $lines->pluck('phone')->filter(function ($value) {
return !empty($value);
})->filter(function ($value) use ($phoneCountries) {
return !$phoneCountries->firstWhere('code', (new PhoneNumber($value))->getCountry());
})) {
if ($phones->isNotEmpty()) {
$this->errors['Some phone numbers are not correct'] = $phones->join(', ', ' and ');
}
}
$existingPhones = Account::whereIn('phone', $lines->pluck('phone')->all())
->pluck('phone');
if ($existingPhones->isNotEmpty()) {
$this->errors['Those phones numbers already exists'] = $existingPhones->join(', ', ' and ');
}
// Emails
if ($emails = $lines->pluck('email')->filter(function ($value) {
return $value != '' && !filter_var($value, FILTER_VALIDATE_EMAIL);
})) {
if ($emails->isNotEmpty()) {
$this->errors['Some emails are not correct'] = $emails->join(', ', ' and ');
}
}
$existingEmails = Account::whereIn('email', $lines->pluck('email')->all())
->pluck('email');
if ($existingEmails->isNotEmpty()) {
$this->errors['Those emails numbers already exists'] = $existingEmails->join(', ', ' and ');
}
if ($emails = $lines->pluck('email')->filter(fn (string $value) => $value != '')->duplicates()) {
if ($emails->isNotEmpty()) {
$this->errors['Those emails are declared several times'] = $emails->join(', ', ' and ');
}
}
// External account
$checkExternalUsernameDomains = collect();
foreach ($lines as $line) {
if ($line->external_username != null && ($line->external_password == null || $line->external_domain == null)) {
$this->errors['Line ' . $line->line . ': The mandatory external account columns must be filled'] = '';
}
if ($line->external_username != null && $line->external_password != null && $line->external_domain != null) {
if ($line->external_domain == $line->external_realm
|| $line->external_domain == $line->external_registrar
|| $line->external_domain == $line->external_outbound_proxy) {
$this->errors['Line ' . $line->line . ': External realm, registrar or outbound proxy must be different than domain'] = '';
}
if (!in_array($line->external_protocol, ExternalAccount::PROTOCOLS)) {
$this->errors['Line ' . $line->line . ': External protocol must be UDP, TCP or TLS'] = '';
}
}
$checkExternalUsernameDomains->push($line->external_username . ',' . $line->external_domain);
}
foreach ($checkExternalUsernameDomains->duplicates() as $duplicate) {
$this->errors['The following external account is used several times: ' . $duplicate] = '';
}
$filePath = $this->errors->isEmpty()
? Storage::putFile($this->importDirectory, $request->file('csv'))
: null;
if ($filePath == false) {
$this->errors['The CSV file was not imported properly on the server'] = '';
}
return view('admin.account.import.check', [
'linesCount' => $lines->count(),
'errors' => $this->errors,
'domain' => $domain,
'filePath' => $filePath
]);
}
public function handle(Request $request)
{
$request->validate([
'file_path' => 'required',
'domain' => 'required|exists:spaces,domain'
]);
$domain = $request->user()->superAdmin
? $request->get('domain')
: $request->user()->domain;
$lines = $this->csvToCollection(storage_path('app/' . $request->get('file_path')));
$accounts = [];
$now = \Carbon\Carbon::now();
$admins = $phones = $passwords = $externals = [];
$externalAlgorithm = 'MD5';
foreach ($lines as $line) {
if ($line->role == 'admin') {
array_push($admins, $line->username);
}
if (!empty($line->phone)) {
$phones[$line->username] = $line->phone;
}
if (!empty($line->password)) {
$passwords[$line->username] = $line->password;
}
if (!empty($line->external_username)) {
$externals[$line->username] = [
'username' => $line->external_username,
'domain' => $line->external_domain,
'realm' => $line->external_realm,
'registrar' => $line->external_registrar,
'outbound_proxy' => $line->external_outbound_proxy,
'protocol' => $line->external_protocol,
'password' => bchash(
$line->external_username,
$line->external_realm ?? $line->external_domain,
$line->external_password,
$externalAlgorithm
),
'algorithm' => $externalAlgorithm,
'created_at' => Carbon::now(),
];
}
array_push($accounts, [
'username' => $line->username,
'domain' => $domain,
'email' => $line->email,
'activated' => $line->status == 'active',
'ip_address' => '127.0.0.1',
'user_agent' => 'CSV Import',
'created_at' => $now,
'updated_at' => $now,
]);
}
Account::insert($accounts);
// Set admins accounts
foreach ($admins as $username) {
$account = Account::where('username', $username)
->where('domain', $domain)
->first();
$account->admin = true;
$account->save();
}
// Set passwords
$passwordsToInsert = [];
$passwordAccounts = Account::whereIn('username', array_keys($passwords))
->where('domain', $domain)
->get();
$algorithm = config('app.account_default_password_algorithm');
foreach ($passwordAccounts as $passwordAccount) {
array_push($passwordsToInsert, [
'account_id' => $passwordAccount->id,
'password' => bchash(
$passwordAccount->username,
$request->space?->account_realm ?? $domain,
$passwords[$passwordAccount->username],
$algorithm
),
'algorithm' => $algorithm
]);
}
Password::insert($passwordsToInsert);
// Set external account
$externalAccountsToInsert = [];
$externalAccounts = Account::whereIn('username', array_keys($externals))
->where('domain', $domain)
->get();
foreach ($externalAccounts as $externalAccount) {
array_push($externalAccountsToInsert, [
'account_id' => $externalAccount->id
] + $externals[$externalAccount->username]);
}
ExternalAccount::insert($externalAccountsToInsert);
// Set phone accounts
foreach ($phones as $username => $phone) {
$account = Account::where('username', $username)
->where('domain', $domain)
->first();
$account->phone = $phone;
}
return redirect()->route('admin.account.index');
}
private function csvToCollection($file): Collection
{
$lines = collect();
$csv = fopen($file, 'r');
$i = 1;
while (!feof($csv)) {
if ($line = fgetcsv($csv, 1000, ',')) {
if (count($line) != 13) {
$this->errors['Parsing error at line ' . $i] = 'The number of columns is incorrect';
$i++;
continue;
}
$lines->push((object)[
'line' => $i,
'username' => !empty($line[0]) ? $line[0] : null,
'password' => !empty($line[1]) ? $line[1] : null,
'role' => $line[2],
'status' => $line[3],
'phone' => !empty($line[4]) ? $line[4] : null,
'email' => $line[5],
'external_username' => !empty($line[6]) ? $line[6] : null,
'external_domain' => !empty($line[7]) ? $line[7] : null,
'external_password' => !empty($line[8]) ? $line[8] : null,
'external_realm' => !empty($line[9]) ? $line[9] : null,
'external_registrar' => !empty($line[10]) ? $line[10] : null,
'external_outbound_proxy' => !empty($line[11]) ? $line[11] : null,
'external_protocol' => $line[12],
]);
$i++;
}
}
fclose($csv);
$lines->shift();
return $lines;
}
}

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

@ -27,8 +27,9 @@ use App\Account;
use App\ContactsList;
use App\Http\Requests\Account\Create\Web\AsAdminRequest;
use App\Http\Requests\Account\Update\Web\AsAdminRequest as WebAsAdminRequest;
use App\Libraries\FlexisipRedisConnector;
use App\Services\AccountService;
use App\SipDomain;
use App\Space;
class AccountController extends Controller
{
@ -39,8 +40,7 @@ class AccountController extends Controller
'order_sort' => 'in:asc,desc',
]);
$accounts = Account::with('contactsLists')
->orderBy($request->get('order_by', 'updated_at'), $request->get('order_sort', 'desc'));
$accounts = Account::orderBy($request->get('order_by', 'updated_at'), $request->get('order_sort', 'desc'));
if ($request->has('search')) {
$accounts = $accounts->where('username', 'like', '%' . $request->get('search') . '%');
@ -61,6 +61,9 @@ class AccountController extends Controller
}
return view('admin.account.index', [
'space' => (!$request->user()->superAdmin)
? $request->user()->space
: null,
'domains' => Account::groupBy('domain')->pluck('domain'),
'contacts_lists' => ContactsList::all()->pluck('title', 'id'),
'accounts' => $accounts->paginate(20)->appends($request->query()),
@ -72,13 +75,33 @@ class AccountController extends Controller
return redirect()->route('admin.account.index', $request->except('_token', 'query'));
}
public function show(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.show', [
'account' => $account,
'devices' => (new FlexisipRedisConnector)->getDevices($account->identifier)
]);
}
public function create(Request $request)
{
$account = new Account;
if ($request->has('admin')) {
$account->admin = true;
}
if ($request->has('domain')) {
$account->domain = $request->get('domain');
}
return view('admin.account.create_edit', [
'account' => new Account,
'account' => $account,
'domains' => $request->user()?->superAdmin
? SipDomain::all()
: SipDomain::where('domain', $request->user()->domain)->get(),
? Space::notFull()->get()
: Space::where('domain', $request->user()->domain)->get(),
'protocols' => [null => 'None'] + Account::$dtmfProtocols
]);
}
@ -89,7 +112,7 @@ class AccountController extends Controller
Log::channel('events')->info('Web Admin: Account created', ['id' => $account->identifier]);
return redirect()->route('admin.account.edit', $account->id);
return redirect()->route('admin.account.show', $account);
}
public function edit(Request $request, int $accountId)
@ -98,15 +121,10 @@ class AccountController extends Controller
return view('admin.account.create_edit', [
'account' => $account,
'protocols' => [null => 'None'] + Account::$dtmfProtocols,
'protocols' => [null => __('Empty')] + Account::$dtmfProtocols,
'domains' => $request->user()?->superAdmin
? SipDomain::all()
: SipDomain::where('domain', $account->domain)->get(),
'contacts_lists' => ContactsList::whereNotIn('id', function ($query) use ($accountId) {
$query->select('contacts_list_id')
->from('account_contacts_list')
->where('account_id', $accountId);
})->get()
? Space::all()
: Space::where('domain', $account->domain)->get(),
]);
}
@ -116,7 +134,7 @@ class AccountController extends Controller
Log::channel('events')->info('Web Admin: Account updated', ['id' => $account->identifier]);
return redirect()->route('admin.account.edit', $accountId);
return redirect()->route('admin.account.show', $accountId);
}
public function provision(int $accountId)
@ -160,7 +178,7 @@ class AccountController extends Controller
$account->contactsLists()->detach([$request->get('contacts_list_id')]);
$account->contactsLists()->attach([$request->get('contacts_list_id')]);
return redirect()->route('admin.account.edit', $accountId)->withFragment('#contacts_lists');
return redirect()->route('admin.account.contact.index', $accountId)->withFragment('#contacts_lists');
}
public function contactsListRemove(Request $request, int $accountId)
@ -168,6 +186,6 @@ class AccountController extends Controller
$account = Account::findOrFail($accountId);
$account->contactsLists()->detach([$request->get('contacts_list_id')]);
return redirect()->route('admin.account.edit', $accountId)->withFragment('#contacts_lists');
return redirect()->route('admin.account.contact.index', $accountId)->withFragment('#contacts_lists');
}
}

View file

@ -1,241 +0,0 @@
<?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;
use App\Account;
use App\Http\Controllers\Controller;
use Illuminate\Support\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
class AccountImportController extends Controller
{
private Collection $errors;
private string $importDirectory = 'imported_csv';
public function __construct()
{
$this->errors = collect();
}
public function create(Request $request)
{
return view('admin.account.import.create', [
'domains' => Account::select('domain')->distinct()->get()->pluck('domain')
]);
}
public function store(Request $request)
{
$request->validate([
'csv' => ['required', File::types(['csv', 'txt'])],
'domain' => 'required|exists:accounts'
]);
$lines = $this->csvToCollection($request->file('csv'));
/**
* Error checking
*/
// Usernames
$existingUsernames = Account::where('domain', $request->get('domain'))
->whereIn('username', $lines->pluck('username')->all())
->pluck('username');
if ($existingUsernames->isNotEmpty()) {
$this->errors['Those usernames already exists'] = $existingUsernames->join(', ', ' and ');
}
if ($duplicates = $lines->pluck('username')->duplicates()) {
if ($duplicates->isNotEmpty()) {
$this->errors['Those usernames are declared several times'] = $duplicates->join(', ', ' and ');
}
}
if ($lines->pluck('username')->contains(function ($value) {
return strlen($value) < 2;
})) {
$this->errors['Some usernames are shorter than expected'] = '';
}
// Passwords
if ($lines->pluck('password')->contains(function ($value) {
return strlen($value) < 6;
})) {
$this->errors['Some passwords are shorter than expected'] = '';
}
// Roles
if ($lines->pluck('role')->contains(function ($value) {
return !in_array($value, ['admin', 'user']);
})) {
$this->errors['Some roles are not correct'] = '';
}
// Status
if ($lines->pluck('status')->contains(function ($value) {
return !in_array($value, ['active', 'inactive']);
})) {
$this->errors['Some status are not correct'] = '';
}
// Phones
if ($phones = $lines->pluck('phone')->filter(function ($value) {
return strlen($value) > 2 && substr($value, 0, 1) != '+';
})) {
if ($phones->isNotEmpty()) {
$this->errors['Some phone numbers are not correct'] = $phones->join(', ', ' and ');
}
}
$existingPhones = Account::whereIn('phone', $lines->pluck('phone')->all())
->pluck('phone');
if ($existingPhones->isNotEmpty()) {
$this->errors['Those phones numbers already exists'] = $existingPhones->join(', ', ' and ');
}
// Emails
if ($emails = $lines->pluck('email')->filter(function ($value) {
return !filter_var($value, FILTER_VALIDATE_EMAIL);
})) {
if ($emails->isNotEmpty()) {
$this->errors['Some emails are not correct'] = $emails->join(', ', ' and ');
}
}
$existingEmails = Account::whereIn('email', $lines->pluck('email')->all())
->pluck('email');
if ($existingEmails->isNotEmpty()) {
$this->errors['Those emails numbers already exists'] = $existingEmails->join(', ', ' and ');
}
if ($emails = $lines->pluck('email')->duplicates()) {
if ($emails->isNotEmpty()) {
$this->errors['Those emails are declared several times'] = $emails->join(', ', ' and ');
}
}
$filePath = $this->errors->isEmpty()
? Storage::putFile($this->importDirectory, $request->file('csv'))
: null;
return view('admin.account.import.check', [
'linesCount' => $lines->count(),
'errors' => $this->errors,
'domain' => $request->get('domain'),
'filePath' => $filePath
]);
}
public function handle(Request $request)
{
$request->validate([
'file_path' => 'required',
'domain' => 'required|exists:accounts'
]);
$lines = $this->csvToCollection(storage_path('app/' . $request->get('file_path')));
$accounts = [];
$now = \Carbon\Carbon::now();
$admins = $phones = [];
foreach ($lines as $line) {
if ($line->role == 'admin') {
array_push($admins, $line->username);
}
if (!empty($line->phone)) {
$phones[$line->username] = $line->phone;
}
array_push($accounts, [
'username' => $line->username,
'domain' => $request->get('domain'),
'email' => $line->email,
'activated' => $line->status == 'active',
'ip_address' => '127.0.0.1',
'user_agent' => 'CSV Import',
'created_at' => $now,
'updated_at' => $now,
]);
}
Account::insert($accounts);
// Set admins accounts
foreach ($admins as $username) {
$account = Account::where('username', $username)
->where('domain', $request->get('domain'))
->first();
$account->admin = true;
}
// Set admins accounts
foreach ($phones as $username => $phone) {
$account = Account::where('username', $username)
->where('domain', $request->get('domain'))
->first();
$account->phone = $phone;
}
return redirect()->route('admin.account.index');
}
private function csvToCollection($file): Collection
{
$lines = collect();
$csv = fopen($file, 'r');
$i = 1;
while (!feof($csv)) {
if ($line = fgetcsv($csv, 1000, ',')) {
$lines->push((object)[
'line' => $i,
'username' => $line[0],
'password' => $line[1],
'role' => $line[2],
'status' => $line[3],
'phone' => $line[4],
'email' => $line[5],
]);
$i++;
}
}
fclose($csv);
$lines->shift();
return $lines;
}
}

View file

@ -0,0 +1,95 @@
<?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\Controllers\Admin;
use App\ApiKey;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Carbon\Carbon;
class ApiKeyController extends Controller
{
public function index(Request $request)
{
return view('admin.api_key.index', [
'api_keys' => $this->getApiKeysQuery($request)->with('account')->get()
]);
}
public function create(Request $request)
{
return view('admin.api_key.create', [
'account' => $request->user()
]);
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|min:3',
'expires_after_last_used_minutes' => 'integer|min:0'
]);
$apiKey = new ApiKey;
$apiKey->account_id = $request->user()->id;
$apiKey->name = $request->get('name');
$apiKey->expires_after_last_used_minutes = $request->get('expires_after_last_used_minutes');
$apiKey->last_used_at = Carbon::now();
$apiKey->key = Str::random(40);
$apiKey->save();
return redirect()->route('admin.api_keys.index');
}
public function delete(Request $request, string $key)
{
return view('admin.api_key.delete', [
'api_key' => $this->getApiKeysQuery($request)->where('key', $key)->first()
]);
}
public function destroy(Request $request)
{
$this->getApiKeysQuery($request)->where('key', $request->get('key'))->delete();
return redirect()->route('admin.api_keys.index');
}
private function getApiKeysQuery(Request $request)
{
$apiKeys = ApiKey::whereIn('account_id', function ($query) {
$query->select('id')
->from('accounts')
->where('admin', true);
})->whereNotNull('expires_after_last_used_minutes');
if (!$request->user()->superAdmin) {
$apiKeys->whereIn('account_id', function ($query) use ($request) {
$query->select('id')
->from('accounts')
->where('domain', $request->user()->domain);
});
}
return $apiKeys;
}
}

View file

@ -0,0 +1,64 @@
<?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;
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)
{
$account = Account::findOrFail($accountId);
return view('admin.account.external.show', [
'account' => $account,
'externalAccount' => $account->external ?? new ExternalAccount,
'protocols' => ExternalAccount::PROTOCOLS
]);
}
public function store(CreateUpdate $request, int $accountId)
{
(new AccountService)->storeExternalAccount($request, $accountId);
return redirect()->route('admin.account.show', $accountId);
}
public function delete(int $accountId)
{
return view('admin.account.external.delete', [
'account' => Account::findOrFail($accountId)
]);
}
public function destroy(int $accountId)
{
(new AccountService)->deleteExternalAccount($accountId);
return redirect()->route('admin.account.show', $accountId);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Account;
use App\Http\Controllers\Controller;
use App\Mail\Provisioning;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class ProvisioningEmailController extends Controller
{
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.provisioning_email.create', [
'account' => $account
]);
}
public function send(int $accountId)
{
$account = Account::findOrFail($accountId);
$account->provision();
Mail::to($account)->send(new Provisioning($account));
Log::channel('events')->info('Web Admin: Sending provisioning email', ['id' => $account->identifier]);
return redirect()->route('admin.account.show', $account);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Account;
use App\ResetPasswordEmailToken;
use App\Http\Controllers\Controller;
use App\Mail\ResetPassword;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class ResetPasswordEmailController extends Controller
{
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.reset_password_email.create', [
'account' => $account
]);
}
public function send(int $accountId)
{
$account = Account::findOrFail($accountId);
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;
$resetPasswordEmail->token = Str::random(16);
$resetPasswordEmail->email = $account->email;
$resetPasswordEmail->save();
Mail::to($account)->send(new ResetPassword($account));
return redirect()->route('admin.account.activity.index', $account);
}
}

View file

@ -1,120 +0,0 @@
<?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;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\SipDomain;
use Illuminate\Validation\Rule;
class SipDomainController extends Controller
{
public function index()
{
return view('admin.sip_domain.index', ['sip_domains' => SipDomain::withCount('accounts')->get()]);
}
public function create()
{
return view('admin.sip_domain.create_edit', [
'sip_domain' => new SipDomain
]);
}
public function store(Request $request)
{
$request->validate([
'domain' => 'required|unique:sip_domains',
]);
$sipDomain = new SipDomain;
$sipDomain->domain = $request->get('domain');
$sipDomain = $this->setConfig($request, $sipDomain);
$sipDomain->save();
return redirect()->route('admin.sip_domains.index');
}
public function edit(int $id)
{
return view('admin.sip_domain.create_edit', [
'sip_domain' => SipDomain::findOrFail($id)
]);
}
public function update(Request $request, int $id)
{
$request->validate([
'max_account' => 'required|integer',
]);
$sipDomain = SipDomain::findOrFail($id);
$sipDomain = $this->setConfig($request, $sipDomain);
$sipDomain->save();
return redirect()->back();
}
private function setConfig(Request $request, SipDomain $sipDomain)
{
$request->validate([
'max_account' => 'required|integer',
]);
$sipDomain->super = getRequestBoolean($request, 'super');
$sipDomain->disable_chat_feature = getRequestBoolean($request, 'disable_chat_feature');
$sipDomain->disable_meetings_feature = getRequestBoolean($request, 'disable_meetings_feature');
$sipDomain->disable_broadcast_feature = getRequestBoolean($request, 'disable_broadcast_feature');
$sipDomain->hide_settings = getRequestBoolean($request, 'hide_settings');
$sipDomain->max_account = $request->get('max_account', 0);
$sipDomain->hide_account_settings = getRequestBoolean($request, 'hide_account_settings');
$sipDomain->disable_call_recordings_feature = getRequestBoolean($request, 'disable_call_recordings_feature');
$sipDomain->only_display_sip_uri_username = getRequestBoolean($request, 'only_display_sip_uri_username');
$sipDomain->assistant_hide_create_account = getRequestBoolean($request, 'assistant_hide_create_account');
$sipDomain->assistant_disable_qr_code = getRequestBoolean($request, 'assistant_disable_qr_code');
$sipDomain->assistant_hide_third_party_account = getRequestBoolean($request, 'assistant_hide_third_party_account');
return $sipDomain;
}
public function delete(int $id)
{
return view('admin.sip_domain.delete', [
'sip_domain' => SipDomain::findOrFail($id)
]);
}
public function destroy(Request $request, int $id)
{
$sipDomain = SipDomain::findOrFail($id);
$request->validate([
'domain' => [
'required',
Rule::in(['first-zone', $sipDomain->domain]),
]
]);
$sipDomain->delete();
return redirect()->route('admin.sip_domains.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,65 +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 show(int $id)
public function create(Request $request, Space $space)
{
}
public function create(Request $request)
{
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') . '%');
@ -87,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' => [
@ -103,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

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Admin\Space;
use App\Space;
use App\SpaceEmailServer;
use App\Http\Requests\EmailServer\CreateUpdate;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class EmailServerController extends Controller
{
public function show(int $spaceId)
{
$space = Space::findOrFail($spaceId);
return view('admin.space.email_server.show', [
'space' => $space,
'emailServer' => $space->emailServer ?? new SpaceEmailServer
]);
}
public function store(CreateUpdate $request, int $spaceId)
{
$space = Space::findOrFail($spaceId);
$emailServer = $space->emailServer ?? new SpaceEmailServer;
$emailServer->space_id = $space->id;
$emailServer->host = $request->get('host');
$emailServer->port = $request->get('port');
$emailServer->username = $request->get('username');
$emailServer->password = $request->get('password');
$emailServer->from_address = $request->get('from_address') ?? null;
$emailServer->from_name = $request->get('from_name') ?? null;
$emailServer->signature = $request->get('signature') ?? null;
$emailServer->save();
return redirect()->route('admin.spaces.integration', $spaceId);
}
public function delete(int $spaceId)
{
$space = Space::findOrFail($spaceId);
return view('admin.space.email_server.delete', [
'space' => $space
]);
}
public function destroy(int $spaceId)
{
$space = Space::findOrFail($spaceId);
$space->emailServer->delete();
return redirect()->route('admin.spaces.integration', $spaceId);
}
}

View file

@ -0,0 +1,226 @@
<?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;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\Create;
use App\Space;
use App\Rules\Ini;
use App\Rules\Domain;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class SpaceController extends Controller
{
public function index()
{
return view('admin.space.index', ['spaces' => Space::withCount('accounts')->orderBy('host')->get()]);
}
public function me(Request $request)
{
return view('admin.space.show', [
'space' => $request->user()->space
]);
}
public function show(Space $space)
{
return view('admin.space.show', [
'space' => $space
]);
}
public function create()
{
return view('admin.space.create', [
'space' => new Space()
]);
}
public function store(Create $request)
{
$fullHost = empty($request->get('host'))
? config('app.root_host')
: $request->get('host') . '.' . config('app.root_host');
$request->merge(['full_host' => $fullHost]);
$request->validate([
'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/',
'full_host' => ['required', 'unique:spaces,host', new Domain()],
]);
$space = new Space();
$space->name = $request->get('name');
$space->domain = $request->get('domain');
$space->host = $request->get('full_host');
$space->account_realm = $request->get('account_realm');
$space->save();
return redirect()->route('admin.spaces.index');
}
public function edit(Space $space)
{
return view('admin.space.edit', [
'space' => $space
]);
}
public function update(Request $request, Space $space)
{
$request->validate([
'max_account' => 'required|integer',
]);
$space = $this->setAppConfiguration($request, $space);
$space->save();
return redirect()->back();
}
public function configuration(Space $space)
{
return view('admin.space.configuration', [
'space' => $space
]);
}
public function integration(Space $space)
{
return view('admin.space.integration', [
'space' => $space
]);
}
public function configurationUpdate(Request $request, Space $space)
{
$space = $this->setConfiguration($request, $space);
$space->save();
return redirect()->route('admin.spaces.configuration', $space);
}
public function administration(Space $space)
{
return view('admin.space.administration', [
'space' => $space
]);
}
public function administrationUpdate(Request $request, Space $space)
{
$request->validate([
'name' => ['required', Rule::unique('spaces')->ignore($space->id)],
'max_accounts' => 'required|integer|min:0',
'expire_at' => 'nullable|date|after_or_equal:today'
]);
if ($request->get('max_accounts') > 0) {
$request->validate([
'max_accounts' => 'integer|min:' . $space->accounts()->count()
]);
}
$space->name = $request->get('name');
$space->super = getRequestBoolean($request, 'super');
$space->max_accounts = $request->get('max_accounts');
$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);
}
private function setConfiguration(Request $request, Space $space)
{
$request->validate([
'newsletter_registration_address' => 'nullable|email',
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)]
]);
$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');
if ($space->accounts()->count() == 0) {
$space->account_realm = $request->get('account_realm');
}
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
$space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all');
$space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header');
$space->public_registration = getRequestBoolean($request, 'public_registration');
$space->phone_registration = getRequestBoolean($request, 'phone_registration');
$space->intercom_features = getRequestBoolean($request, 'intercom_features');
return $space;
}
private function setAppConfiguration(Request $request, Space $space)
{
$request->validate([
'max_account' => 'required|integer',
]);
$space->disable_chat_feature = getRequestBoolean($request, 'disable_chat_feature', reversed: true);
$space->disable_meetings_feature = getRequestBoolean($request, 'disable_meetings_feature', reversed: true);
$space->disable_broadcast_feature = getRequestBoolean($request, 'disable_broadcast_feature', reversed: true);
$space->hide_settings = getRequestBoolean($request, 'hide_settings', reversed: true);
$space->max_account = $request->get('max_account', 0);
$space->hide_account_settings = getRequestBoolean($request, 'hide_account_settings', reversed: true);
$space->disable_call_recordings_feature = getRequestBoolean($request, 'disable_call_recordings_feature', reversed: true);
$space->only_display_sip_uri_username = getRequestBoolean($request, 'only_display_sip_uri_username');
$space->assistant_hide_create_account = getRequestBoolean($request, 'assistant_hide_create_account', reversed: true);
$space->assistant_disable_qr_code = getRequestBoolean($request, 'assistant_disable_qr_code', reversed: true);
$space->assistant_hide_third_party_account = getRequestBoolean($request, 'assistant_hide_third_party_account', reversed: true);
return $space;
}
public function delete(Request $request, int $id)
{
$space = Space::findOrFail($id);
return view('admin.space.delete', [
'space' => $space
]);
}
public function destroy(Request $request, int $id)
{
$space = Space::findOrFail($id);
$request->validate([
'domain' => [
'required',
Rule::in(['first-zone', $space->domain]),
]
]);
$space->delete();
return redirect()->route('admin.spaces.index');
}
}

View file

@ -106,9 +106,13 @@ class StatisticsController extends Controller
$fromQuery = StatisticsCall::query();
$toQuery = StatisticsCall::query();
if ($request->get('domain')) {
$fromQuery->where('to_domain', $request->get('domain'));
$toQuery->where('from_domain', $request->get('domain'));
$domain = $request->user()->superAdmin
? $request->get('domain')
: $request->space->domain;
if ($domain) {
$fromQuery->where('to_domain', $domain);
$toQuery->where('from_domain', $domain);
}
if ($request->get('to')) {

View file

@ -20,29 +20,12 @@
namespace App\Http\Controllers\Api\Account;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use App\Http\Requests\Account\Create\Api\Request as ApiRequest;
use App\Account;
use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Http\Requests\Account\Create\Api\Request as ApiRequest;
use App\Libraries\OvhSMS;
use App\Mail\RegisterConfirmation;
use App\Rules\AccountCreationToken as RulesAccountCreationToken;
use App\Rules\AccountCreationTokenNotExpired;
use App\Rules\BlacklistedUsername;
use App\Rules\FilteredPhone;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\PasswordAlgorithm;
use App\Services\AccountService;
class AccountController extends Controller
@ -81,256 +64,11 @@ class AccountController extends Controller
return abort(404, 'No TURN service configured');
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function phoneInfo(Request $request, string $phone)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->merge(['phone' => $phone]);
$request->validate([
'phone' => ['required', 'phone', new FilteredPhone]
]);
$account = Account::where('domain', config('app.sip_domain'))
->where(function ($query) use ($phone) {
$query->where('username', $phone)
->orWhere('phone', $phone);
})->firstOrFail();
return \response()->json([
'activated' => $account->activated,
'realm' => $account->realm,
'phone' => (bool)$account->phone
]);
}
/**
* /!\ Dangerous endpoint, disabled by default
* Store directly the account and alias in the DB and send a SMS or email for the validation
*/
public function storePublic(Request $request)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->validate([
'username' => [
'required_without:phone',
new NoUppercase,
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
$query->where('domain', $request->has('domain') ? $request->get('domain') : config('app.sip_domain'));
}),
Rule::unique('accounts_tombstones', 'username')->where(function ($query) use ($request) {
$query->where('domain', $request->has('domain') ? $request->get('domain') : config('app.sip_domain'));
}),
'filled',
],
'algorithm' => ['required', new PasswordAlgorithm],
'password' => 'required|filled',
'domain' => 'min:3',
'email' => config('app.account_email_unique')
? 'required_without:phone|email|unique:accounts,email'
: 'required_without:phone|email',
'phone' => [
'required_without:email',
'required_without:username',
'phone',
new FilteredPhone,
'unique:accounts,phone',
'unique:accounts,username',
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken,
new AccountCreationTokenNotExpired
]
]);
$account = new Account;
$account->username = !empty($request->get('username'))
? $request->get('username')
: $request->get('phone');
$account->email = $request->get('email');
$account->activated = false;
$account->domain = $request->has('domain')
? $request->get('domain')
: config('app.sip_domain');
$account->ip_address = $request->ip();
$account->created_at = Carbon::now();
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
$account->save();
$account->updatePassword($request->get('password'), $request->get('algorithm'));
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
$token->consume();
$token->account_id = $account->id;
$token->save();
Log::channel('events')->info('API deprecated - Store public: AccountCreationToken redeemed', ['account_creation_token' => $token->toLog()]);
Log::channel('events')->info('API deprecated - Store public: Account created', ['id' => $account->identifier]);
// Send validation by phone
if ($request->has('phone')) {
$account->phone = $request->get('phone');
$account->confirmation_key = generatePin();
$account->save();
Log::channel('events')->info('API deprecated: Account created using the public endpoint by phone', ['id' => $account->identifier]);
$ovhSMS = new OvhSMS;
$ovhSMS->send($request->get('phone'), 'Your ' . config('app.name') . ' creation code is ' . $account->confirmation_key);
} elseif ($request->has('email')) {
// Send validation by email
$account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize);
$account->save();
Log::channel('events')->info('API deprecated - Store public: Account created using the public endpoint by email', ['id' => $account->identifier]);
try {
Mail::to($account)->send(new RegisterConfirmation($account));
} catch (\Exception $e) {
Log::channel('events')->info('API deprecated - Store public: Public Register Confirmation email not sent, check errors log', ['id' => $account->identifier]);
Log::error('Public Register Confirmation email not sent: ' . $e->getMessage());
}
}
// Full reload
return Account::withoutGlobalScopes()->find($account->id);
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function recoverByPhone(Request $request)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->validate([
'phone' => [
'required', 'phone', new FilteredPhone, 'exists:accounts,phone'
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken,
new AccountCreationTokenNotExpired
]
]);
$account = Account::where('phone', $request->get('phone'))->first();
$account->confirmation_key = generatePin();
$account->save();
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
$token->consume();
$token->account_id = $account->id;
$token->save();
Log::channel('events')->info('API deprecated - Account recovery: AccountCreationToken redeemed', ['account_creation_token' => $token->toLog()]);
Log::channel('events')->info('API deprecated - Account recovery: Account recovery by phone', ['id' => $account->identifier]);
$ovhSMS = new OvhSMS;
$ovhSMS->send($request->get('phone'), 'Your ' . config('app.name') . ' recovery code is ' . $account->confirmation_key);
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function recoverUsingKey(string $sip, string $recoveryKey)
{
if (!config('app.dangerous_endpoints')) return abort(404);
list($username, $domain) = explode('@', $sip);
$account = Account::where('domain', $domain)
->where(function ($query) use ($username) {
$query->where('username', $username)
->orWhere('phone', $username);
})->firstOrFail();
$confirmationKey = $account->confirmation_key;
$account->confirmation_key = null;
if ($confirmationKey != $recoveryKey) abort(404);
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->save();
$account->passwords->each(function ($i, $k) {
$i->makeVisible(['password']);
});
return $account;
}
public function store(ApiRequest $request)
{
return (new AccountService)->store($request);
}
/**
* Deprecated
*/
public function activateEmail(Request $request, string $sip)
{
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([
'confirmation_key' => 'required|size:' . WebAuthenticateController::$emailCodeSize
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();
Log::channel('events')->info('API: Account activated by email', ['id' => $account->identifier]);
return $account;
}
/**
* Deprecated
*/
public function activatePhone(Request $request, string $sip)
{
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([
'confirmation_key' => 'required|digits:4'
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();
Log::channel('events')->info('API: Account activated by phone', ['id' => $account->identifier]);
return $account;
}
public function show(Request $request)
{
return Account::where('id', $request->user()->id)

View file

@ -29,7 +29,7 @@ class ApiKeyController extends Controller
public function generate(Request $request)
{
$account = $request->user();
$account->generateApiKey($request);
$account->generateUserApiKey($request->ip());
$account->refresh();
Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes'));
@ -42,7 +42,7 @@ class ApiKeyController extends Controller
$authToken = AuthToken::where('token', $token)->valid()->firstOrFail();
if ($authToken->account) {
$authToken->account->generateApiKey($request);
$authToken->account->generateUserApiKey($request->ip());
$authToken->account->refresh();
Cookie::queue('x-api-key', $authToken->account->apiKey->key, config('app.api_key_expiration_minutes'));

View file

@ -26,6 +26,9 @@ use Carbon\Carbon;
use App\AccountCreationToken;
use App\AccountCreationRequestToken;
use App\Rules\PnParam;
use App\Rules\PnPrid;
use App\Rules\PnProvider;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Libraries\FlexisipPusherConnector;
@ -36,9 +39,9 @@ class CreationTokenController extends Controller
public function sendByPush(Request $request)
{
$request->validate([
'pn_provider' => 'required',
'pn_param' => 'required',
'pn_prid' => 'required',
'pn_provider' => ['required', new PnProvider],
'pn_param' => [new PnParam],
'pn_prid' => [new PnPrid],
]);
$last = AccountCreationToken::where('pn_provider', $request->get('pn_provider'))
@ -61,10 +64,9 @@ class CreationTokenController extends Controller
$token->pn_prid = $request->get('pn_prid');
$token->fillRequestInfo($request);
// Send the token to the device via Push Notification
$fp = new FlexisipPusherConnector($token->pn_provider, $token->pn_param, $token->pn_prid);
if ($fp->sendToken($token->token)) {
Log::channel('events')->info('API: Token sent', ['token' => $token->token]);
Log::channel('events')->info('API: Account Creation Token sent', ['token' => $token->token]);
$token->save();
return;

View file

@ -20,7 +20,7 @@
namespace App\Http\Controllers\Api\Account;
use App\Http\Controllers\Controller;
use App\Libraries\FlexisipConnector;
use App\Libraries\FlexisipRedisConnector;
use Illuminate\Http\Request;
use stdClass;
@ -28,14 +28,14 @@ class DeviceController extends Controller
{
public function index(Request $request)
{
$devices = (new FlexisipConnector)->getDevices($request->user()->identifier);
$devices = (new FlexisipRedisConnector)->getDevices($request->user()->identifier);
return ($devices->isEmpty()) ? new stdClass : $devices;
}
public function destroy(Request $request, string $uuid)
{
$connector = new FlexisipConnector;
$connector = new FlexisipRedisConnector;
return $connector->deleteDevice($request->user()->identifier, $uuid);
}

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

@ -20,14 +20,11 @@
namespace App\Http\Controllers\Api\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use App\Rules\PasswordAlgorithm;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller
{
public function update(Request $request)
@ -63,9 +60,5 @@ class PasswordController extends Controller
}
$account->updatePassword($request->get('password'), $algorithm);
if (!empty($account->email)) {
Mail::to($account)->send(new ConfirmedRegistration($account));
}
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\Account;
use App\Http\Controllers\Controller;
use App\Libraries\FlexisipPusherConnector;
use App\Rules\PnParam;
use App\Rules\PnPrid;
use App\Rules\PnProvider;
use App\Rules\CallId;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Log;
class PushNotificationController extends Controller
{
public function push(Request $request)
{
$request->validate([
'pn_provider' => ['required', new PnProvider],
'pn_param' => [new PnParam],
'pn_prid' => [new PnPrid],
'type' => ['required', Rule::in(array_keys(FlexisipPusherConnector::$apnsTypes))],
'call_id' => [new CallId],
]);
$fp = new FlexisipPusherConnector($request->get('pn_provider'), $request->get('pn_param'), $request->get('pn_prid'));
if ($fp->send(callId: $request->get('call_id'), type: $request->get('type'))) {
Log::channel('events')->info('API: Push notification sent', [
'call_id' => $request->get('call_id'),
'type' => $request->get('type')
]);
return;
}
abort(503, "Push notification not sent");
}
}

View file

@ -0,0 +1,75 @@
<?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\Controllers\Api\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use App\AccountRecoveryToken;
use App\Rules\PnParam;
use App\Rules\PnPrid;
use App\Rules\PnProvider;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Libraries\FlexisipPusherConnector;
class RecoveryTokenController extends Controller
{
public function sendByPush(Request $request)
{
$request->validate([
'pn_provider' => ['required', new PnProvider],
'pn_param' => [new PnParam],
'pn_prid' => [new PnPrid],
]);
$last = AccountRecoveryToken::where('pn_provider', $request->get('pn_provider'))
->where('pn_param', $request->get('pn_param'))
->where('pn_prid', $request->get('pn_prid'))
->where('created_at', '>=', Carbon::now()->subMinutes(config('app.account_recovery_token_retry_minutes'))->toDateTimeString())
->where('used', true)
->latest()
->first();
if ($last) {
Log::channel('events')->info('API: Token throttled', ['token' => $last->token]);
abort(429, 'Last token requested too recently');
}
$token = new AccountRecoveryToken;
$token->token = Str::random(WebAuthenticateController::$emailCodeSize);
$token->pn_provider = $request->get('pn_provider');
$token->pn_param = $request->get('pn_param');
$token->pn_prid = $request->get('pn_prid');
$token->fillRequestInfo($request);
$fp = new FlexisipPusherConnector($token->pn_provider, $token->pn_param, $token->pn_prid);
if ($fp->sendToken($token->token)) {
Log::channel('events')->info('API: AccountRecoveryToken sent', ['token' => $token->token]);
$token->save();
return;
}
abort(503, "Token not sent");
}
}

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)
@ -30,6 +30,6 @@ class VcardsStorageController extends Controller
public function destroy(Request $request, string $uuid)
{
return (new AdminVcardsStorageController)->destroy($request->user()->id, $uuid);
return (new AdminVcardsStorageController)->destroy($request, $request->user()->id, $uuid);
}
}

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,44 +17,44 @@
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)
{
if (Account::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
}
$account = $request->space->accounts()->findOrFail($accountId);
$account->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
return Account::findOrFail($id)->contacts()->attach($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)
{
if (!Account::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->contacts()->pluck('id')->contains($contactId)) {
abort(404);
}
return Account::findOrFail($id)->contacts()->detach($contactId);
return $account->contacts()->detach($contactId);
}
}

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

@ -21,27 +21,36 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
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;
use App\ResetPasswordEmailToken;
use App\Http\Requests\Account\Create\Api\AsAdminRequest;
use App\Http\Requests\Account\Update\Api\AsAdminRequest as ApiAsAdminRequest;
use App\Mail\Provisioning;
use App\Mail\ResetPassword;
use App\Services\AccountService;
use App\SipDomain;
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) {
@ -56,19 +65,29 @@ class AccountController extends Controller
return $account;
}
public function search(string $sip)
public function search(Request $request, string $sip)
{
return Account::sip($sip)->firstOrFail();
$account = $request->space->accounts()->sip($sip)->first();
if (!$account)
abort(404, 'SIP address not found');
return $account;
}
public function searchByEmail(string $email)
public function searchByEmail(Request $request, string $email)
{
return Account::where('email', $email)->firstOrFail();
$account = $request->space->accounts()->where('email', $email)->first();
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();
@ -82,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();
@ -93,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();
@ -104,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();
@ -115,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();
@ -126,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();
@ -139,15 +158,6 @@ class AccountController extends Controller
public function store(AsAdminRequest $request)
{
// Create the missing SipDomain
if ($request->user()->superAdmin
&& $request->has('domain')
&& !SipDomain::pluck('domain')->contains($request->get('domain'))) {
$sipDomain = new SipDomain();
$sipDomain->domain = $request->get('domain');
$sipDomain->save();
}
return (new AccountService())->store($request)->makeVisible(['confirmation_key', 'provisioning_token']);
}
@ -157,46 +167,80 @@ class AccountController extends Controller
Log::channel('events')->info('API Admin: Account updated', ['id' => $account->identifier]);
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
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(Request $request, int $accountId)
{
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email)
abort(403, 'No email configured');
$account->provision();
Mail::to($account)->send(new Provisioning($account));
Log::channel('events')->info('API: Sending provisioning email', ['id' => $account->identifier]);
}
public function sendResetPasswordEmail(Request $request, int $accountId)
{
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email)
abort(403, 'No email configured');
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;
$resetPasswordEmail->token = Str::random(16);
$resetPasswordEmail->email = $account->email;
$resetPasswordEmail->save();
Mail::to($account)->send(new ResetPassword($account));
}
}

View file

@ -20,23 +20,29 @@
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Libraries\FlexisipConnector;
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 FlexisipConnector)->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 FlexisipConnector;
$connector = new FlexisipRedisConnector;
return $connector->deleteDevice(Account::findOrFail($accountId)->identifier, $uuid);
return $connector->deleteDevice(
$request->space->accounts()->findOrFail($accountId)->identifier,
$uuid
);
}
}

View file

@ -0,0 +1,43 @@
<?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\Api\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExternalAccount\CreateUpdate;
use App\Services\AccountService;
use Illuminate\Http\Request;
class ExternalAccountController extends Controller
{
public function show(Request $request, int $accountId)
{
return $request->space->accounts()->findOrFail($accountId)->external()->firstOrFail();
}
public function store(CreateUpdate $request, int $accountId)
{
return (new AccountService)->storeExternalAccount($request, $accountId);
}
public function destroy(int $accountId)
{
return (new AccountService)->deleteExternalAccount($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

@ -1,111 +0,0 @@
<?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\Api\Admin;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\SipDomain;
class SipDomainController extends Controller
{
public function index()
{
return SipDomain::all();
}
public function store(Request $request)
{
$request->validate([
'domain' => 'required|unique:sip_domains',
]);
$sipDomain = new SipDomain;
$sipDomain->domain = $request->get('domain');
$this->setRequestBoolean($request, $sipDomain, 'super');
$this->setRequestBoolean($request, $sipDomain, 'disable_chat_feature');
$this->setRequestBoolean($request, $sipDomain, 'disable_meetings_feature');
$this->setRequestBoolean($request, $sipDomain, 'disable_broadcast_feature');
$this->setRequestBoolean($request, $sipDomain, 'hide_settings');
$this->setRequestBoolean($request, $sipDomain, 'hide_account_settings');
$this->setRequestBoolean($request, $sipDomain, 'disable_call_recordings_feature');
$this->setRequestBoolean($request, $sipDomain, 'only_display_sip_uri_username');
$this->setRequestBoolean($request, $sipDomain, 'assistant_hide_create_account');
$this->setRequestBoolean($request, $sipDomain, 'assistant_disable_qr_code');
$this->setRequestBoolean($request, $sipDomain, 'assistant_hide_third_party_account');
$sipDomain->max_account = $request->get('max_account', 0);
$sipDomain->save();
return $sipDomain->refresh();
}
public function show(string $domain)
{
return SipDomain::where('domain', $domain)->firstOrFail();
}
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',
]);
$sipDomain = SipDomain::where('domain', $domain)->firstOrFail();
$sipDomain->super = $request->get('super');
$sipDomain->disable_chat_feature = $request->get('disable_chat_feature');
$sipDomain->disable_meetings_feature = $request->get('disable_meetings_feature');
$sipDomain->disable_broadcast_feature = $request->get('disable_broadcast_feature');
$sipDomain->hide_settings = $request->get('hide_settings');
$sipDomain->hide_account_settings = $request->get('hide_account_settings');
$sipDomain->disable_call_recordings_feature = $request->get('disable_call_recordings_feature');
$sipDomain->only_display_sip_uri_username = $request->get('only_display_sip_uri_username');
$sipDomain->assistant_hide_create_account = $request->get('assistant_hide_create_account');
$sipDomain->assistant_disable_qr_code = $request->get('assistant_disable_qr_code');
$sipDomain->assistant_hide_third_party_account = $request->get('assistant_hide_third_party_account');
$sipDomain->max_account = $request->get('max_account', 0);
$sipDomain->save();
return $sipDomain;
}
private function setRequestBoolean(Request $request, SipDomain $sipDomain, string $key)
{
if ($request->has($key)) {
$sipDomain->$key = (bool)$request->get($key);
}
}
public function destroy(string $domain)
{
return SipDomain::where('domain', $domain)->delete();
}
}

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,29 +67,30 @@ 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)
{
if (ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
}
$contactsList = $request->space->contactsLists()->findOrFail($id);
$contactsList->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
return ContactsList::findOrFail($id)->contacts()->attach($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)
{
if (!ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
$contactsList = $request->space->contactsLists()->findOrFail($id);
if (!$contactsList->contacts()->pluck('id')->contains($contactId)) {
abort(404);
}
return ContactsList::findOrFail($id)->contacts()->detach($contactId);
return $contactsList->contacts()->detach($contactId);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Api\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\EmailServer\CreateUpdate;
use App\Space;
use App\SpaceEmailServer;
use Illuminate\Http\Request;
class EmailServerController extends Controller
{
public function show(string $domain)
{
return Space::where('domain', $domain)->firstOrFail()->emailServer()->firstOrFail();
}
public function store(CreateUpdate $request, string $domain)
{
$space = Space::where('domain', $domain)->firstOrFail();
$emailServer = $space->emailServer ?? new SpaceEmailServer;
$emailServer->space_id = $space->id;
$emailServer->host = $request->get('host');
$emailServer->port = $request->get('port');
$emailServer->username = $request->get('username');
$emailServer->password = $request->get('password');
$emailServer->from_address = $request->get('from_address') ?? null;
$emailServer->from_name = $request->get('from_name') ?? null;
$emailServer->signature = $request->get('signature') ?? null;
$emailServer->save();
return $emailServer;
}
public function destroy(string $domain)
{
$space = Space::where('host', $domain)->firstOrFail();
return $space->emailServer->delete();
}
}

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