Compare commits

...

54 commits

Author SHA1 Message Date
Timothée Jaussoin
f3e06bb125 Fix FLEXIAPI-412 Restrict the default messages statistics graph to the space... 2025-11-25 11:39:15 +00:00
Timothée Jaussoin
f035b720cd Fix FLEXIAPI-406 Add an artisan console script to clear statistics after n days 2025-11-24 10:47:35 +00:00
Timothée Jaussoin
da63835902 Fix FLEXIAPI-404 Prevent the account domain to be reset during creation on error 2025-10-15 11:19:35 +02:00
Timothée Jaussoin
80d10bed7a Fix FLEXIAPI-402 Handle empty emails cases when importing accounts, handle... 2025-10-14 09:30:36 +00:00
Timothée Jaussoin
992761c1d0 Fix FLEXIAPI-355 Add withoutGlobalScope() to the Account ContactVcardList resolver 2025-10-14 09:04:07 +00:00
Timothée Jaussoin
5082654d01 Fix FLEXIAPI-401 Use a Space scoped reset password URL 2025-10-13 14:41:44 +02:00
Timothée Jaussoin
92978bf256 Fix FLEXIAPI-398 List all the availables Spaces domains when importing, fix... 2025-10-08 09:32:11 +00:00
Timothée Jaussoin
84fdb380d7 Fix FLEXIAPI-396 Remove the CHANGELOG.md file (redundant with the Git history)... 2025-10-07 10:04:37 +00:00
Timothée Jaussoin
d9b0f83e5d Fix FLEXIAPI-394 Apply SpaceCheck on all the pages and URLs, backport from 2.1 2025-09-29 15:26:25 +02:00
Timothée Jaussoin
7ae237eb7c Fix FLEXIAPI-392 Fix the recover_by_code view and use the account space object 2025-09-29 07:00:11 +00:00
Timothée Jaussoin
e888d53d31 Fix FLEXIAPI-391 Add missing account view attribute in the actions.delete view 2025-09-24 15:05:18 +02:00
Timothée Jaussoin
18cdf04026 Fix FLEXIAPI-390 Use the properly resolved Space domain when applying the... 2025-09-24 09:19:11 +00:00
Timothée Jaussoin
3b0dd419bb Fix FLEXIAPI-387 Ensure that INSTANCE_CONFIRMED_REGISTRATION_TEXT et... 2025-09-18 13:55:46 +00:00
Timothée Jaussoin
ef110eb4ce Fix FLEXIAPI-385 Use domains and not hosts in the EmailServer endpoints as... 2025-09-11 15:18:58 +02:00
Timothée Jaussoin
2006fa0929 Fix FLEXIAPI-378 Return a valid JSON containing the vCard and not the raw... 2025-08-28 13:56:07 +02:00
Timothée Jaussoin
7e9d34cf0b Fix FLEXIAPI-375 Fix VcardsStorage table UUID size, recover the UUID from the stored vCard 2025-08-27 16:36:16 +02:00
Timothée Jaussoin
740f603bbe Fix FLEXIAPI-372 Remove SESSION_DRIVER and CACHE_DRIVER and enforce them to file 2025-08-26 14:05:05 +02:00
Timothée Jaussoin
6af68374d6 Fix FLEXIAPI-371 Add documentation for the Wizard page 2025-08-18 17:05:15 +02:00
Timothée Jaussoin
fe89060698 Fix FLEXIAPI-361 Prepare the 2.0 release 2025-08-04 15:26:39 +02:00
Timothée Jaussoin
80246a232e Fix FLEXIAPI-364 Fix a faulty redirection in the ExternalAccount controller 2025-07-23 16:53:11 +02:00
Timothée Jaussoin
0578125f70 Fix FLEXIAPI-363 Send the Redis publish event when the externalAccount is... 2025-07-23 13:25:32 +00:00
Timothée Jaussoin
8882cdab18 Fix FLEXIAPI-312 Add Redis publish event when updating the externalAccount to... 2025-07-23 08:44:31 +00:00
Timothée Jaussoin
058d253dbc Fix FLEXIAPI-362 Return an empty object and not an empty array in the... 2025-07-22 08:18:53 +00:00
Timothée Jaussoin
2d611a1e33 Fix FLEXIAPI-360 Add rules on some jobs to only run them in the Gitlab pipeline when needed 2025-07-16 16:24:45 +02:00
Timothée Jaussoin
695d36e279 Fix FLEXIAPI-354 Fix contact deletion 2025-07-15 11:53:47 +02:00
Timothée Jaussoin
f5d6abc836 Fix FLEXIAPI-355 Add withoutGlobalScope() to the Account ContactVcardList resolver
Update the dependencies
Add missing rockylinux versions in deploy.yml for some jobs
2025-07-10 16:52:36 +02:00
Timothée Jaussoin
f37bc45194 Fix FLEXIAPI-356 Cleanup and reorganize the pipeline to mutualize some things and save time 2025-07-10 15:54:28 +02:00
Timothée Jaussoin
40c8209cdc Fix FLEXIAPI-353 Validate UUIDs in the vcards-storage endpoints, complete the... 2025-07-09 14:57:48 +00:00
Timothée Jaussoin
d55cc59e19 Fix FLEXIAPI-352 Add missing errors box in the password change form 2025-07-08 17:09:36 +02:00
Timothée Jaussoin
5771e39a9e Fix/351 import accounts windows 2025-07-08 14:24:02 +00:00
Timothée Jaussoin
dcd1719a61 Fix FLEXIAPI-350 Fix wrongly assigned variables in some views 2025-07-08 15:14:31 +02:00
Timothée Jaussoin
aa2051d281 Fix FLEXIAPI-348 Add a fallback 404 page for URLs that are pointing to no configured Spaces 2025-07-03 15:42:15 +02:00
Timothée Jaussoin
80f32a9887 Feature/346 ini info 2025-07-03 10:05:17 +00:00
Timothée Jaussoin
336c037590 Fix FLEXIAPI-342 Enforce password change when the External Account domain is changed 2025-07-02 17:19:57 +02:00
Timothée Jaussoin
d2cac6d60f Fix FLEXIAPI-341 Allow realm to be empty when creating a Space 2025-07-02 10:26:47 +02:00
Timothée Jaussoin
c8e587db01 Fix FLEXIAPI-341 Allow realm to be empty when creating a Space 2025-07-01 12:04:49 +02:00
Timothée Jaussoin
f712755b79 Fix FLEXIAPI-340 Fix the space resolution when getting the realm on Accounts 2025-06-30 15:49:56 +02:00
Jonathan Bartet
2e11db0c83 Fix FLEXIAPI-326 Rework email templates and translations 2025-06-30 12:00:31 +02:00
Timothée Jaussoin
bd4dbfeb7a Fix FLEXIAPI-337 Generate the provisioning URLs based on the user space 2025-06-24 13:04:59 +00:00
Timothée Jaussoin
37baeb2fc2 Fix FLEXIAPI-333 Remove HTML buttons because they cannot be rendered in "old" Outlook versions 2025-06-23 14:16:48 +00:00
Timothée Jaussoin
3a7f4486bb Fix FLEXIAPI-336 Fix broken ph icons 2025-06-19 11:08:00 +02:00
Timothée Jaussoin
f561a55221 Fix FLEXIAPI-335 Safari rendering issues with font icons 2025-06-19 10:23:49 +02:00
Timothée Jaussoin
73c0132051 Fix FLEXIAPI-324 Add an app setup wizard page 2025-06-17 16:06:29 +02:00
Timothée Jaussoin
4f3e9c57c9 Fix FLEXIAPI-330 Remove the ConfirmedRegistration email and related code 2025-06-17 10:58:35 +02:00
Timothée Jaussoin
f20056ec73 Fix FLEXIAPI-329 Use correct routes for accounts devices 2025-06-16 17:19:39 +02:00
Timothée Jaussoin
6b589d1d0f Fix FLEXIAPI-332 Check if the first line was untouched and that the number of... 2025-06-16 14:38:00 +00:00
Timothée Jaussoin
0348eecf4c Fix FLEXIAPI-325 Add endpoints to send the password reset and provisioning emails 2025-06-11 17:19:55 +02:00
Timothée Jaussoin
52eadf91ce Fix FLEXIAPI-328 Set realm on Space creation, limit the update if some accounts are present 2025-06-11 11:11:23 +02:00
Timothée Jaussoin
39992c0bd5 Fix FLEXIAPI-322 Api Keys documentation 2025-06-09 14:13:18 +02:00
Timothée Jaussoin
6b22d2ec86 Fix FLEXIAPI-321 Disable the account creation button when the Space is full for admins 2025-06-09 12:00:28 +02:00
Timothée Jaussoin
fae800343f Fix FLEXIAPI-319 Fix the admin device deletion link, recover the missing method 2025-06-09 09:59:59 +00:00
Timothée Jaussoin
92825b0211 Fix FLEXIAPI-318 Fix email recovery validation 2025-06-05 15:08:51 +02:00
Timothée Jaussoin
d4a757a892 Fix FLEXIAPI-313 Fix the admin device deletion link, recover the missing... 2025-06-05 11:16:09 +02:00
Timothée Jaussoin
f1c0247960 Fix/311 316 317 320 2025-06-04 14:35:15 +00:00
154 changed files with 1770 additions and 1427 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,7 +9,7 @@ rocky8-deploy:
rocky9-deploy:
extends: .deploy
script:
- ./deploy_packages.sh rockylinux
- ./deploy_packages.sh rockylinux 9
needs:
- rocky9-package
- rocky9-test
@ -24,26 +24,30 @@ debian12-deploy:
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
rules:
- changes:
- .gitlab-ci.yml
script:
- ./deploy_packages.sh rockylinux
- ./deploy_packages.sh rockylinux 9
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,4 +1,6 @@
rocky8-package:
needs:
- prepare-package
extends: .package
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
script:
@ -8,9 +10,11 @@ rocky8-package:
- 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:
@ -20,16 +24,18 @@ rocky9-package:
- dnf -y module enable php:remi-8.2
- dnf -y update php\*
- dnf -y install php-sodium
- make rpm-el9
- make package-el9
debian12-package:
needs:
- prepare-package
extends: .debian_package
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_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
@ -51,6 +57,10 @@ remi-rocky9-package:
.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
@ -88,7 +98,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,19 +3,16 @@ 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
.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
@ -36,31 +33,12 @@ debian12-test:
- 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,17 +1,19 @@
variables:
ROCKY_8_IMAGE_VERSION: 20241113_143521_update_php_82
ROCKY_9_IMAGE_VERSION: 20250513_111901_upgrade_packages
ROCKY_8_IMAGE_VERSION: 20250702_171834_update_rocky8_dockerhub
ROCKY_9_IMAGE_VERSION: 20250702_171314_update_rocky9_dockerhub
DEBIAN_12_IMAGE_VERSION: 20241204_162237_update_download_linphone_org
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,199 +1,142 @@
# Flexisip Account Manager Changelog
# Releases
v2.0
----
- Fix FLEXIAPI-205 Remove the deprecated endpoints, compatibility code documentation and tests. Drop the confirmation_key accounts column and activation_expirations table
- Fix FLEXIAPI-206 Upgrade to Laravel 10, PHP 8.1 minimum and bump all the related dependencies, drop Debian 11 Bullseye
- Fix FLEXIAPI-220 Migrate SIP Domains to Spaces
- Fix GH-15 Add password import from CSV
- Fix FLEXIAPI-242 Add stricter validation for the AccountCreationToken Push Notification endpoint
- Fix FLEXIAPI-241 Add a /push-notification endpoint to send custom push notifications to the Flexisip Pusher
- Fix FLEXIAPI-244 Remove faulty middleware
- Fix FLEXIAPI-250 Allow Spaces to be declared without a subdomain
- Fix FLEXIAPI-252 Update the hCaptcha Laravel library, use file instead of cookies to store the session to prevent empty errors bags
- Fix FLEXIAPI-254 Allow no data on POST requests to not trigger the ValidateJSON middleware
- Fix FLEXIAPI-255 Create a INSTALL.md tutorial and log FlexisipPusherConnector errors
- Fix FLEXIAPI-257 Return a more coherent message when search API endpoints returns a 404
- Fix FLEXIAPI-260 Return 404 and not 403 if the contact is already in the list or missing when removing it
- Fix FLEXIAPI-262 Bypass the JWT auth if we have an API Key
- Fix FLEXIAPI-264 Add -k|api_key_ip parameter to accounts:create-admin-account to set/clear the related API Key restriction
- Fix FLEXIAPI-256 Publish an empty string while deleting a device on Redis to force the refresh on the other clients
- Fix FLEXIAPI-268 Allow pn-param in Apple format for the push notifications endpoints
- Fix FLEXIAPI-269 Update the IsNotPhoneNumber rule to use a better phone number validator
- Fix FLEXIAPI-258 Move DotEnv instance configurations in the Spaces table
- Fix FLEXIAPI-270 Call the static $apnsTypes attribute in FlexisipPusherConnector
- Fix FLEXIAPI-271 Handle properly reversed attributes in objects
- Fix FLEXIAPI-237 Add internationalisation support in the app
- Fix FLEXIAPI-261 Remove the TURN part in the XML provisioning (and only keep the API endpoint)
- Fix FLEXIAPI-275 Add names in Spaces
- Fix FLEXIAPI-278 Complete and reorganize the Markdown documentation
- Fix FLEXIAPI-233 Add External Accounts (new version)
- Fix FLEXIAPI-277 Restrict authorized ini keys that can be set to prevent conflict with the existing ones set in the UI
- Fix FLEXIAPI-272 Add Space based email server integration
- Fix FLEXIAPI-284 Add configurable admin API Keys
- Fix FLEXIAPI-232 Add provisioning email + important redesign of the contacts page
- Fix FLEXIAPI-287 Refactor the emails templates
- Fix FLEXIAPI-286 Send an account_recovery_token using a push notification and protect the account recovery using phone page with the account_recovery_token
- Fix FLEXIAPI-293 Remove the (long) outdated general documentation
- Fix FLEXIAPI-224 Add a console script to send Space Expiration emails
- Fix FLEXIAPI-297 Fix PrId and CallId validations
- Fix FLEXIAPI-305 Add specific error page for Space Expiration
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 spaces 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.0]
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...
- **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.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
- **Complete and reorganize the Markdown documentation**
- **Refactor the emails templates** All the emails were modernized and are now generated in HTML
v1.4.5
------
- Fix FLEXIAPI-132 Refactor the Provisioning to remove proxy_default_values
### Removed
v1.4.4
------
- Fix FLEXIAPI-136 Refactor the Web Panel toggle mechanism and move it to a proper Middleware
- **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.4.3
------
- Fix FLEXIAPI-133 Use the correct breadcrumb on create and fix a password update related issue on update
### Migrate from [1.6]
v1.4.2
------
- Fix #135 Refactor the password algorithms code
1. Deploy the new version and migrate the database.
v1.4.1
------
- Fix #133 Make the MySQL connection unstrict
```
php artisan migrate
```
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
2. Set `APP_ROOT_HOST` in `.env` or as an environnement variable. And remove `APP_URL` and `APP_SIP_DOMAIN`
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
```
APP_ROOT_HOST=myhost.com
```
v1.2
----
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`.
- 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
```
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.

View file

@ -2,7 +2,7 @@
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.
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`.
@ -33,7 +33,7 @@ To know more about the web server configuration part, you can directly [visit th
# 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 overwritting them in your Docker or web-server 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
@ -74,7 +74,7 @@ For example:
## 5. Create a first administrator and finish the setup
Create a first administator account:
Create a first administrator account:
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=}

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
@ -58,11 +58,13 @@ 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:
mkdir -p build
sed -i 's/Requires:.*/Requires: php >= 8.1, php-gd, php-pdo, php-redis, php-mysqlnd, php-mbstring/g' $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild -v -bb --define 'dist .el8' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
@echo "📦✅ RPM el8 Package Created"
rpm-el9-only:
mkdir -p build
rpmbuild -v -bb --define 'dist .el9' --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
@echo "📦✅ RPM el9 Package Created"
@ -72,6 +74,7 @@ rpm-cleanup:
rm -r rpmbuild
deb-only:
mkdir -p build
sed -i 's/posttrans/post/g' $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
rpmbuild -v -bb --with deb --define '_topdir $(OUTPUT_DIR)/rpmbuild' --define "_rpmfilename tmp.rpm" --define "_rpmdir $(OUTPUT_DIR)/rpmbuild" $(OUTPUT_DIR)/rpmbuild/SPECS/flexisip-account-manager.spec
fakeroot alien -g -k --scripts $(OUTPUT_DIR)/rpmbuild/tmp.rpm
@ -88,11 +91,18 @@ 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-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

View file

@ -17,7 +17,7 @@ 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
@ -25,7 +25,6 @@ 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.
@ -41,7 +40,7 @@ Create or update a Space, required to then create accounts afterward. The `super
### Import the old DotEnv instance configuration into a Space
Since 1.7 some environnement instance configuration variables were moved into the Space configuration, you can import them using this command.
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}
@ -95,19 +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:
- 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.

View file

@ -1,117 +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/).
## [2.0]
### Added
- **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.
### Changed
- **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_CUSTOM_THEME
- INSTANCE_CONFIRMED_REGISTRATION_TEXT
- 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
### Migrate from [1.6]
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.
### Deprecated
- **Last major version supporting the deprecated endpoints of the API**
## [1.6] - 2024-12-30
### Added
- **Phone validation** Phone numbers are now strictly validated and countries can be enabled disabled to prevent spam
- **SIP Domains** Account SIP domains can now be managed from the UI and API
- **CoTURN Credential** Get CoTURN credentials from the API
- **RFC 8898 Support**
### 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.

View file

@ -6,3 +6,4 @@ 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 spaces:expiration-emails
sudo -su www-data && php artisan app:clear-statistics 30 --apply

View file

@ -5,4 +5,5 @@ 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 spaces:expiration-emails
php artisan spaces:expiration-emails
php artisan app:clear-statistics 30 --apply

View file

@ -13,11 +13,6 @@ 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
# 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
@ -63,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=file
SESSION_LIFETIME=120
# SMTP and emails

View file

@ -73,7 +73,7 @@ class Account extends Authenticatable
return;
}
$builder->where('domain', config('app.sip_domain'));
$builder->where('domain', config('app.sip_domain') ?? space()->domain);
});
}
@ -266,6 +266,18 @@ class Account extends Authenticatable
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();
@ -322,12 +334,12 @@ class Account extends Authenticatable
public function getRealmAttribute()
{
return space()?->account_realm;
return $this->space->account_realm;
}
public function getResolvedRealmAttribute()
{
return space()?->account_realm ?? $this->domain;
return $this->space->account_realm ?? $this->domain;
}
public function getConfirmationKeyExpiresAttribute()
@ -354,6 +366,34 @@ class Account extends Authenticatable
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
*/

View file

@ -46,10 +46,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

@ -36,7 +36,7 @@ class ClearApiKeys extends Command
if ($minutes == 0) {
$this->info('Expiration time is set to 0, nothing to clear');
return 0;
return Command::SUCCESS;
}
$this->info('Deleting user API Keys unused after ' . $minutes . ' minutes');

View file

@ -53,10 +53,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

@ -94,6 +94,6 @@ class CreateAdminAccount extends Command
$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

@ -68,6 +68,6 @@ class CreateAdminTest extends Command
$this->info('API Key updated to: ' . $secret);
return 0;
return Command::SUCCESS;
}
}

View file

@ -39,12 +39,12 @@ class SetAdmin extends Command
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 +52,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

@ -44,14 +44,21 @@ class CreateUpdate extends Command
$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 domain already exists, updating it')
: $this->info('A new domain will be created');
? $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 domain')
: $this->info('Set as a normal domain');
? $this->info('Set as a super Space')
: $this->info('Set as a normal Space');
$space->save();

View file

@ -16,8 +16,7 @@ class ImportConfigurationFromDotEnv extends Command
if (!$space) {
$this->error('The space cannot be found');
return 0;
return Command::SUCCESS;
}
$this->info('The following configuration will be imported in the space ' . $space->domain);
@ -30,7 +29,6 @@ class ImportConfigurationFromDotEnv extends Command
$space->copyright_text = env('INSTANCE_COPYRIGHT', null);
$space->intro_registration_text = env('INSTANCE_INTRO_REGISTRATION', null);
$space->confirmed_registration_text = env('INSTANCE_CONFIRMED_REGISTRATION_TEXT', 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);

View file

@ -40,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;
@ -49,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

@ -155,6 +155,12 @@ 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')
@ -173,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)

View file

@ -25,6 +25,19 @@ use App\Libraries\FlexisipRedisConnector;
class DeviceController extends Controller
{
public function index(Request $request)
{
$connector = new FlexisipRedisConnector;
return view(
'account.device.index',
[
'account' => $request->user(),
'devices' => $connector->getDevices($request->user()->identifier)
]
);
}
public function delete(Request $request, string $uuid)
{
$connector = new FlexisipRedisConnector;

View file

@ -21,11 +21,8 @@ namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller
{
public function show(Request $request)
@ -55,10 +52,6 @@ class PasswordController extends Controller
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');
}
}

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()

View file

@ -59,8 +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_if:email|HCaptcha' : '',
'account_recovery_token' => 'required_if:phone',
'h-captcha-response' => captchaConfigured() ? 'required_with:email|HCaptcha' : '',
'account_recovery_token' => 'required_with:phone',
];
$account = null;

View file

@ -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()

View file

@ -49,7 +49,7 @@ class AccountDeviceController extends Controller
'admin.account.device.delete',
[
'account' => $account,
'device' => $connector->getDevices($account->identifier)
'device' => $connector->getDevices($account->identifier)
->where('uuid', $uuid)->first()
]
);
@ -62,6 +62,6 @@ class AccountDeviceController extends Controller
$connector->deleteDevice($account->identifier, $request->get('uuid'));
return redirect()->route('admin.account.device.index', $account);
return redirect()->route('admin.account.show', $account);
}
}

View file

@ -92,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()
]
);
}

View file

@ -23,6 +23,7 @@ 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;
@ -44,7 +45,9 @@ class AccountImportController extends Controller
public function create(Request $request)
{
return view('admin.account.import.create', [
'domains' => Account::select('domain')->distinct()->get()->pluck('domain')
'domains' => $request->user()->superAdmin
? Space::pluck('domain')
: [$request->user()->domain]
]);
}
@ -52,11 +55,29 @@ class AccountImportController extends Controller
{
$request->validate([
'csv' => ['required', File::types(['csv', 'txt'])],
'domain' => 'required|exists:accounts'
'domain' => 'required|exists:spaces,domain'
]);
$lines = $this->csvToCollection($request->file('csv'));
$domain = $request->get('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
@ -132,7 +153,7 @@ class AccountImportController extends Controller
// Emails
if ($emails = $lines->pluck('email')->filter(function ($value) {
return !filter_var($value, FILTER_VALIDATE_EMAIL);
return $value != '' && !filter_var($value, FILTER_VALIDATE_EMAIL);
})) {
if ($emails->isNotEmpty()) {
$this->errors['Some emails are not correct'] = $emails->join(', ', ' and ');
@ -146,7 +167,7 @@ class AccountImportController extends Controller
$this->errors['Those emails numbers already exists'] = $existingEmails->join(', ', ' and ');
}
if ($emails = $lines->pluck('email')->duplicates()) {
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 ');
}
@ -154,6 +175,8 @@ class AccountImportController extends Controller
// 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'] = '';
@ -170,16 +193,26 @@ class AccountImportController extends Controller
$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' => $request->get('domain'),
'domain' => $domain,
'filePath' => $filePath
]);
}
@ -188,9 +221,13 @@ class AccountImportController extends Controller
{
$request->validate([
'file_path' => 'required',
'domain' => 'required|exists:accounts'
'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 = [];
@ -234,7 +271,7 @@ class AccountImportController extends Controller
array_push($accounts, [
'username' => $line->username,
'domain' => $request->get('domain'),
'domain' => $domain,
'email' => $line->email,
'activated' => $line->status == 'active',
'ip_address' => '127.0.0.1',
@ -249,9 +286,10 @@ class AccountImportController extends Controller
// Set admins accounts
foreach ($admins as $username) {
$account = Account::where('username', $username)
->where('domain', $request->get('domain'))
->where('domain', $domain)
->first();
$account->admin = true;
$account->save();
}
// Set passwords
@ -259,7 +297,7 @@ class AccountImportController extends Controller
$passwordsToInsert = [];
$passwordAccounts = Account::whereIn('username', array_keys($passwords))
->where('domain', $request->get('domain'))
->where('domain', $domain)
->get();
$algorithm = config('app.account_default_password_algorithm');
@ -269,7 +307,7 @@ class AccountImportController extends Controller
'account_id' => $passwordAccount->id,
'password' => bchash(
$passwordAccount->username,
space()?->account_realm ?? $request->get('domain'),
space()?->account_realm ?? $domain,
$passwords[$passwordAccount->username],
$algorithm
),
@ -284,7 +322,7 @@ class AccountImportController extends Controller
$externalAccountsToInsert = [];
$externalAccounts = Account::whereIn('username', array_keys($externals))
->where('domain', $request->get('domain'))
->where('domain', $domain)
->get();
foreach ($externalAccounts as $externalAccount) {
@ -298,7 +336,7 @@ class AccountImportController extends Controller
// Set phone accounts
foreach ($phones as $username => $phone) {
$account = Account::where('username', $username)
->where('domain', $request->get('domain'))
->where('domain', $domain)
->first();
$account->phone = $phone;
}
@ -314,6 +352,12 @@ class AccountImportController extends Controller
$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,
@ -329,7 +373,6 @@ class AccountImportController extends Controller
'external_registrar' => !empty($line[10]) ? $line[10] : null,
'external_outbound_proxy' => !empty($line[11]) ? $line[11] : null,
'external_protocol' => $line[12],
]);
$i++;

View file

@ -21,6 +21,7 @@ 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;
@ -42,57 +43,22 @@ class ExternalAccountController extends Controller
public function store(CreateUpdate $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$externalAccount = $account->external ?? new ExternalAccount;
(new AccountService)->storeExternalAccount($request, $accountId);
$password = '';
if ($account->external?->realm != $request->get('realm')) {
$password = 'required_with:realm';
} elseif ($externalAccount->password == null) {
$password = 'required';
}
$request->validate(['password' => $password]);
$algorithm = 'MD5';
$externalAccount->account_id = $account->id;
$externalAccount->username = $request->get('username');
$externalAccount->domain = $request->get('domain');
$externalAccount->realm = $request->get('realm');
$externalAccount->registrar = $request->get('registrar');
$externalAccount->outbound_proxy = $request->get('outbound_proxy');
$externalAccount->protocol = $request->get('protocol');
if (!empty($request->get('password'))) {
$externalAccount->password = bchash(
$externalAccount->username,
$externalAccount->realm ?? $externalAccount->domain,
$request->get('password'),
$algorithm
);
$externalAccount->algorithm = $algorithm;
}
$externalAccount->save();
return redirect()->route('admin.account.show', $account->id);
return redirect()->route('admin.account.show', $accountId);
}
public function delete(int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.external.delete', [
'account' => $account
'account' => Account::findOrFail($accountId)
]);
}
public function destroy(int $accountId)
{
$account = Account::findOrFail($accountId);
$account->external->delete();
(new AccountService)->deleteExternalAccount($accountId);
return redirect()->route('admin.account.show', $account->id);
return redirect()->route('admin.account.show', $accountId);
}
}

View file

@ -20,6 +20,7 @@
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;
@ -55,7 +56,7 @@ class SpaceController extends Controller
]);
}
public function store(Request $request)
public function store(Create $request)
{
$fullHost = empty($request->get('host'))
? config('app.root_host')
@ -63,8 +64,6 @@ class SpaceController extends Controller
$request->merge(['full_host' => $fullHost]);
$request->validate([
'name' => 'required|unique:spaces',
'domain' => ['required', 'unique:spaces', new Domain()],
'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/',
'full_host' => ['required', 'unique:spaces,host', new Domain()],
]);
@ -73,6 +72,7 @@ class SpaceController extends Controller
$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');
@ -160,10 +160,13 @@ class SpaceController extends Controller
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->confirmed_registration_text = $request->get('confirmed_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');
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');

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->user()->domain;
if ($domain) {
$fromQuery->where('to_domain', $domain);
$toQuery->where('from_domain', $domain);
}
if ($request->get('to')) {

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

@ -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

@ -21,14 +21,19 @@ 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\Space;
@ -198,4 +203,36 @@ class AccountController extends Controller
return Account::findOrFail($accountId)->contactsLists()->detach($contactsListId);
}
/**
* Emails
*/
public function sendProvisioningEmail(int $accountId)
{
$account = Account::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(int $accountId)
{
$account = Account::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

@ -11,14 +11,14 @@ use Illuminate\Http\Request;
class EmailServerController extends Controller
{
public function show(string $host)
public function show(string $domain)
{
return Space::where('host', $host)->firstOrFail()->emailServer()->firstOrFail();
return Space::where('domain', $domain)->firstOrFail()->emailServer()->firstOrFail();
}
public function store(CreateUpdate $request, string $host)
public function store(CreateUpdate $request, string $domain)
{
$space = Space::where('host', $host)->firstOrFail();
$space = Space::where('domain', $domain)->firstOrFail();
$emailServer = $space->emailServer ?? new SpaceEmailServer;
$emailServer->space_id = $space->id;
@ -35,9 +35,9 @@ class EmailServerController extends Controller
return $emailServer;
}
public function destroy(string $host)
public function destroy(string $domain)
{
$space = Space::where('host', $host)->firstOrFail();
$space = Space::where('host', $domain)->firstOrFail();
return $space->emailServer->delete();
}
}

View file

@ -21,6 +21,7 @@ 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;
use Illuminate\Validation\Rule;
@ -36,46 +37,11 @@ class ExternalAccountController extends Controller
public function store(CreateUpdate $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$externalAccount = $account->external ?? new ExternalAccount;
$password = '';
if ($account->external?->realm != $request->get('realm')) {
$password = 'required_with:realm';
} elseif ($externalAccount->password == null) {
$password = 'required';
}
$request->validate(['password' => $password]);
$algorithm = 'MD5';
$externalAccount->account_id = $account->id;
$externalAccount->username = $request->get('username');
$externalAccount->domain = $request->get('domain');
$externalAccount->realm = $request->get('realm');
$externalAccount->registrar = $request->get('registrar');
$externalAccount->outbound_proxy = $request->get('outbound_proxy');
$externalAccount->protocol = $request->get('protocol');
$externalAccount->algorithm = $algorithm;
if (!empty($request->get('password'))) {
$externalAccount->password = bchash(
$externalAccount->username,
$externalAccount->realm ?? $externalAccount->domain,
$request->get('password'),
$algorithm
);
}
$externalAccount->save();
return $externalAccount;
return (new AccountService)->storeExternalAccount($request, $accountId);
}
public function destroy(int $accountId)
{
$account = Account::findOrFail($accountId);
return $account->external->delete();
return (new AccountService)->deleteExternalAccount($accountId);
}
}

View file

@ -20,9 +20,10 @@
namespace App\Http\Controllers\Api\Admin;
use App\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\Create;
use App\Rules\Domain;
use App\Rules\Ini;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
@ -34,15 +35,13 @@ class SpaceController extends Controller
return Space::all();
}
public function store(Request $request)
public function store(Create $request)
{
$request->validate([
'name' => 'required|unique:spaces',
'domain' => ['required', 'unique:spaces', new Domain()],
'host' => ['required', 'unique:spaces', new Domain()],
'max_accounts' => 'nullable|integer',
'expire_at' => 'nullable|date|after_or_equal:today',
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)]
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
]);
$space = new Space;
@ -66,7 +65,6 @@ class SpaceController extends Controller
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->confirmed_registration_text = $request->get('confirmed_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');
@ -106,6 +104,7 @@ class SpaceController extends Controller
'max_account' => 'required|integer',
'max_accounts' => 'required|integer',
'expire_at' => 'nullable|date|after_or_equal:today',
'account_realm' => ['nullable', new Domain()],
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
'custom_provisioning_overwrite_all' => 'required|boolean',
@ -149,7 +148,6 @@ class SpaceController extends Controller
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->confirmed_registration_text = $request->get('confirmed_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');

View file

@ -26,12 +26,14 @@ use App\VcardStorage;
use Illuminate\Http\Request;
use Sabre\VObject;
use stdClass;
class VcardsStorageController extends Controller
{
public function index(int $accountId)
{
return Account::findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
$list = Account::findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
return $list->isEmpty() ? new stdClass : $list;
}
public function show(int $accountId, string $uuid)
@ -46,29 +48,40 @@ class VcardsStorageController extends Controller
]);
$vcardo = VObject\Reader::read($request->get('vcard'));
$vcardoUID = substr($vcardo->UID, 9);
if (Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardo->UID)->first()) {
$request->merge(['uuid' => $vcardoUID]);
$request->validate(['uuid' => 'uuid']);
if (Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardoUID)->first()) {
abort(409, 'Vcard already exists');
}
$vcard = new VcardStorage();
$vcard->account_id = $accountId;
$vcard->uuid = $vcardo->UID;
$vcard->uuid = $vcardoUID;
$vcard->vcard = preg_replace('/\r\n?/', "\n", $vcardo->serialize());
$vcard->save();
return $vcard->vcard;
return $vcard;
}
public function update(Request $request, int $accountId, string $uuid)
{
$request->merge(['uuid' => $uuid]);
$request->validate([
'uuid' => 'uuid',
'vcard' => ['required', new Vcard()]
]);
$vcardo = VObject\Reader::read($request->get('vcard'));
$vcardoUID = substr($vcardo->UID, 9);
if ($vcardo->UID != $uuid) {
$request->merge(['vuuid' => $vcardoUID]);
$request->validate(['vuuid' => 'uuid']);
if ($vcardoUID != $uuid) {
abort(422, 'UUID should be the same');
}
@ -76,11 +89,14 @@ class VcardsStorageController extends Controller
$vcard->vcard = preg_replace('/\r\n?/', "\n", $vcardo->serialize());
$vcard->save();
return $vcard->vcard;
return $vcard;
}
public function destroy(int $accountId, string $uuid)
public function destroy(Request $request, int $accountId, string $uuid)
{
$request->merge(['uuid' => $uuid]);
$request->validate(['uuid' => 'uuid']);
$vcard = Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
return $vcard->delete();

View file

@ -52,13 +52,15 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\Localization::class,
'space.check',
],
'api' => [
'throttle:600,1', // move to 600 instead of 60
'bindings',
'validate_json',
'localization'
'localization',
'space.check',
],
];

View file

@ -70,8 +70,6 @@ class AuthenticateDigestOrKey
->where('domain', $domain)
->firstOrFail();
$resolvedRealm = space()?->account_realm ?? $domain;
// DIGEST authentication
if ($request->header('Authorization')) {
@ -95,7 +93,7 @@ class AuthenticateDigestOrKey
'opaque' => 'required|in:'.$this->getOpaque(),
//'uri' => 'in:/'.$request->path(),
'qop' => 'required|in:auth',
'realm' => 'required|in:'.$resolvedRealm,
'realm' => 'required|in:'.$account->resolvedRealm,
'nc' => 'required',
'cnonce' => 'required',
'algorithm' => [
@ -128,7 +126,7 @@ class AuthenticateDigestOrKey
// Hashing and checking
$a1 = $password->algorithm == 'CLRTXT'
? hash($hash, $account->username.':'.$resolvedRealm.':'.$password->password)
? hash($hash, $account->username.':'.$account->resolvedRealm.':'.$password->password)
: $password->password; // username:realm/domain:password
$a2 = hash($hash, $request->method().':'.$auth['uri']);
@ -199,21 +197,20 @@ class AuthenticateDigestOrKey
private function generateAuthHeaders(Account $account, string $nonce): array
{
$headers = [];
$resolvedRealm = space()?->account_realm ?? $account->domain;
foreach ($account->passwords as $password) {
if ($password->algorithm == 'CLRTXT') {
foreach (array_keys(passwordAlgorithms()) as $algorithm) {
array_push(
$headers,
$this->generateAuthHeader($resolvedRealm, $algorithm, $nonce)
$this->generateAuthHeader($account->resolvedRealm, $algorithm, $nonce)
);
}
break;
} elseif (\in_array($password->algorithm, array_keys(passwordAlgorithms()))) {
array_push(
$headers,
$this->generateAuthHeader($resolvedRealm, $password->algorithm, $nonce)
$this->generateAuthHeader($account->resolvedRealm, $password->algorithm, $nonce)
);
}
}

View file

@ -12,20 +12,20 @@ class SpaceCheck
public function handle(Request $request, Closure $next): Response
{
if (empty(config('app.root_host'))) {
return abort(503, 'APP_ROOT_HOST is not configured');
abort(503, 'APP_ROOT_HOST is not configured');
}
$space = space();
$space = space(reload: true);
if ($space) {
if ($space != null) {
if (!str_ends_with($space->host, config('app.root_host'))) {
return abort(503, 'The APP_ROOT_HOST configured does not match with the current root domain');
abort(503, 'The APP_ROOT_HOST configured does not match with the current root domain');
}
Config::set('app.url', '://' . $space->host);
Config::set('app.sip_domain', $space->domain);
if ($request->user() && !$request->user()->superAdmin && $space?->isExpired()) {
if ($space->isExpired()) {
abort($request->expectsJson() ? 403 : 490, 'The related Space has expired');
}
@ -51,6 +51,6 @@ class SpaceCheck
return $next($request);
}
return abort(404, 'Host not configured');
abort(404, 'Host not configured');
}
}

View file

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

View file

@ -20,6 +20,8 @@
namespace App\Libraries;
use App\Device;
use App\ExternalAccount;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
use stdClass;
@ -54,4 +56,21 @@ class FlexisipRedisConnector
Log::error('Redis server issue: ' . $th->getMessage());
}
}
public function pingB2BUA(ExternalAccount $externalAccount): bool
{
try {
Redis::publish('flexisip/B2BUA/account', json_encode([
'username' => $externalAccount->username,
'domain' => $externalAccount->domain,
'identifier' => "$externalAccount->id"
]));
return true;
} catch (\Throwable $th) {
Log::error('Redis server issue: ' . $th->getMessage());
}
return false;
}
}

View file

@ -57,9 +57,9 @@ class StatisticsGraphFactory
$fromQuery = StatisticsMessage::query();
$toQuery = StatisticsMessage::query();
if (!Auth::user()?->isAdmin) {
if (!Auth::user()?->superAdmin) {
$fromQuery->where('from_domain', space()->domain);
$toQuery->toDomain($this->domain);
$toQuery->toDomain(space()->domain);
} elseif ($this->domain) {
$fromQuery->where('from_domain', $this->domain);
$toQuery->toDomain($this->domain);
@ -126,7 +126,7 @@ class StatisticsGraphFactory
// Accounts doesn't have a from and to
$this->domain = $this->domain ?? $this->fromDomain;
if (!Auth::user()?->isAdmin) {
if (!Auth::user()?->admin) {
$this->data->where('domain', space()->domain);
} elseif ($this->domain) {
$this->data->where('domain', $this->domain);

View file

@ -1,53 +0,0 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2025 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\Mail;
use App\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ConfirmedRegistration extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Account $account
) {
}
public function envelope(): Envelope
{
return new Envelope(
subject: $this->account->space->name . ': '. __('Registration confirmed'),
);
}
public function content(): Content
{
return new Content(
markdown: 'mails.confirmed_registration',
);
}
}

View file

@ -23,7 +23,10 @@ class ExpiringSpace extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: $this->space->name . ': '. __('Space is expiring in :days days', ['days' => $this->space->daysLeft]),
subject: __('Your space :space is expiring in :count days', [
'space' => $this->space->name,
'count' => $this->space->daysLeft,
]),
);
}

View file

@ -40,7 +40,7 @@ class NewsletterRegistration extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: $this->account->space->name . ': '. __('Newsletter registration confirmed'),
subject: $this->account->space->name . ': '. __('New newsletter subscription'),
);
}

View file

@ -40,7 +40,7 @@ class Provisioning extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: $this->account->space->name . ': '. __('Provisioning of your device'),
subject: __('Welcome to :space: Start using your account today', ['space' => $this->account->space->name,]),
);
}

View file

@ -40,7 +40,7 @@ class RecoverByCode extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: $this->account->space->name . ': '. __('Account recovery'),
subject: $this->account->space->name . ': '. __('Your account recovery code'),
);
}

View file

@ -40,7 +40,7 @@ class RegisterValidation extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: $this->account->space->name . ': '. __('Account registered'),
subject: $this->account->space->name . ': '. __('Confirm your registration'),
);
}

View file

@ -23,19 +23,23 @@ use App\Account;
use App\AccountCreationToken;
use App\AccountRecoveryToken;
use App\EmailChangeCode;
use App\ExternalAccount;
use App\Http\Requests\Account\Create\Request as CreateRequest;
use App\Http\Requests\Account\Update\Request as UpdateRequest;
use App\Libraries\FlexisipRedisConnector;
use App\Libraries\OvhSMS;
use App\Mail\NewsletterRegistration;
use App\Mail\RecoverByCode;
use App\Mail\RegisterValidation;
use App\PhoneChangeCode;
use App\Rules\FilteredPhone;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rule;
class AccountService
@ -193,8 +197,15 @@ class AccountService
public function destroy(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$externalAccount = $account->external;
$account->delete();
if ($externalAccount) {
(new FlexisipRedisConnector)->pingB2BUA($externalAccount);
}
Log::channel('events')->info(
'Account Service: Account destroyed',
['id' => $account->identifier]
@ -399,4 +410,62 @@ class AccountService
return $account;
}
/**
* External account
*/
public function storeExternalAccount(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$externalAccount = $account->external ?? new ExternalAccount;
$password = '';
if ($account->external?->realm != $request->get('realm')) {
$password = 'required_with:realm';
} elseif ($account->external?->domain != $request->get('domain')) {
$password = 'required_with:domain';
} elseif ($externalAccount->password == null) {
$password = 'required';
}
$request->validate(['password' => $password]);
$algorithm = 'MD5';
$externalAccount->account_id = $account->id;
$externalAccount->username = $request->get('username');
$externalAccount->domain = $request->get('domain');
$externalAccount->realm = $request->get('realm');
$externalAccount->registrar = $request->get('registrar');
$externalAccount->outbound_proxy = $request->get('outbound_proxy');
$externalAccount->protocol = $request->get('protocol');
$externalAccount->algorithm = $algorithm;
if (!empty($request->get('password'))) {
$externalAccount->password = bchash(
$externalAccount->username,
$externalAccount->realm ?? $externalAccount->domain,
$request->get('password'),
$algorithm
);
}
$externalAccount->save();
(new FlexisipRedisConnector)->pingB2BUA($externalAccount);
return $externalAccount;
}
public function deleteExternalAccount(int $accountId)
{
$account = Account::findOrFail($accountId);
$externalAccount = $account->external;
if ($externalAccount) {
(new FlexisipRedisConnector)->pingB2BUA($externalAccount);
return $externalAccount->delete();
}
}
}

View file

@ -43,7 +43,6 @@ class Space extends Model
'assistant_hide_third_party_account',
'copyright_text',
'intro_registration_text',
'confirmed_registration_text',
'newsletter_registration_address',
'account_proxy_registrar_address',
'account_realm'
@ -95,7 +94,7 @@ class Space extends Model
return (int)($this->accounts()->count() / $this->max_accounts * 100);
}
return 0;
return Command::SUCCESS;
}
public function isFull(): bool

972
flexiapi/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ return [
|
*/
'default' => env('CACHE_DRIVER', 'file'),
'default' => 'file',
/*
|--------------------------------------------------------------------------

View file

@ -18,7 +18,7 @@ return [
|
*/
'driver' => env('SESSION_DRIVER', 'cookie'),
'driver' => 'file',
/*
|--------------------------------------------------------------------------

View file

@ -30,11 +30,10 @@ class PasswordFactory extends Factory
public function definition()
{
$account = Account::factory()->create();
$realm = space()?->account_realm ?? $account->domain;
return [
'account_id' => $account->id,
'password' => hash('md5', $account->username.':'.$realm.':testtest'),
'password' => hash('md5', $account->username.':'.$account->resolvedRealm.':testtest'),
'algorithm' => 'MD5',
];
}
@ -54,10 +53,9 @@ class PasswordFactory extends Factory
{
return $this->state(function (array $attributes) {
$account = Account::find($attributes['account_id']);
$realm = space()?->account_realm ?? $account->domain;
return [
'password' => hash('sha256', $account->username.':'.$realm.':testtest'),
'password' => hash('sha256', $account->username.':'.$account->resolvedRealm.':testtest'),
'account_id' => $account->id,
'algorithm' => 'SHA-256',
];

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('spaces', function (Blueprint $table) {
$table->dropColumn('confirmed_registration_text');
});
}
public function down(): void
{
Schema::table('spaces', function (Blueprint $table) {
$table->text('confirmed_registration_text')->nullable();
});
}
};

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\VcardStorage;
use Sabre\VObject;
return new class extends Migration
{
public function up(): void
{
Schema::table('vcards_storage', function (Blueprint $table) {
$table->string('uuid', 45)->change();
});
foreach (VcardStorage::all() as $vcardStorage) {
$vcard = VObject\Reader::read($vcardStorage->vcard);
$vcardStorage->uuid = $vcard->UID;
$vcardStorage->save();
}
}
public function down(): void
{
Schema::table('vcards_storage', function (Blueprint $table) {
$table->string('uuid', 36)->change();
});
}
};

View file

@ -1,58 +1,66 @@
{
"A verification code was sent by email to :email.": "Un code de vérification a été envoyé par email à :email",
"A verification code was sent by SMS to :phone.": "Un code de vérification a été envoyé par SMS au :phone",
"About": "À Propos",
"Account creation": "Création de compte",
"Account recovered recently, try again later": "Tentative de récupération de compte récente, retentez ultérieurement",
"Account Recovery Request":"Demande de récupération de compte",
"Account recovery": "Récupération de compte",
"Account registered": "Compte créé",
"Account settings": "Paramètres de compte",
"Actions": "Actions",
"Activate All": "Tout activer",
"Activate": "Activer",
"Activated": "Activé",
"Activity": "Activité",
"Activity expiration delay": "Délais d'expiration après activité",
"Activity": "Activité",
"Add contact": "Ajout d'un contact",
"Add contacts": "Ajouter des contacts",
"Add existing contacts lists to display them in the user applications.": "Ajouter des listes de contacts existantes afin quelles soient visibles dans les applications de lutilisateur.",
"Add": "Ajouter",
"Admin": "Administrateur",
"Admins": "Administrateurs",
"Administration": "Administration",
"Admins": "Administrateurs",
"All the admins will be super admins": "Tous les administrateurs seront super-administrateurs",
"Allow a custom CSS theme": "Autoriser un thème CSS personnalisé",
"Allow client settings to be overwritten by the provisioning ones": "Écraser la configuration client avec celle du déploiement",
"An email will be sent to :email with a unique link allowing the user to reset its password.": "Un email sera envoyé à :email avec un lien unique l'invitant à réinitialiser son mot de passe",
"An email will be sent to :email with a QR Code and provisioning link.": "Un email sera envoyé à :email contenant un QR Code et un lien de déploiement.",
"An email will be sent to :email with a unique link allowing the user to reset its password.": "Un email sera envoyé à :email avec un lien unique l'invitant à réinitialiser son mot de passe",
"An email will be sent to this email when someone join the newsletter": "Un email sera envoyé à cette addresse quand quelqu'un rejoint la liste de diffusion",
"Api Keys": "Clefs d'API",
"App Configuration": "Configuration de l'App",
"App settings": "Paramètres d'application",
"Api Keys": "Clefs d'API",
"Assistant": "Assistant",
"Best regards,":"Cordialement,",
"Blocked": "Bloqué",
"Broadcast": "Broadcast",
"By email": "Inscription par email",
"By phone": "Par téléphone",
"By": "Par",
"Calls logs": "Journaux d'appel",
"Call Recording": "Enregistrement d'appels",
"Calls logs": "Journaux d'appel",
"Cancel": "Annuler",
"Cannot be changed once created.": "Ne peut être changé par la suite.",
"Change your email": "Changer votre email",
"Change your phone number": "Changer votre numéro de téléphone",
"Code Verification" : "Vérification du code",
"instant messaging": "messagerie instantanée",
"Check the README.md documentation": "Voir la documentation dans README.md",
"Checkout the cheatsheets to know how to format things correctly.": "Jetez un œil à l'aide-mémoire pour découvrir comment formater correctement ce champ.",
"Clear to never expire": "Laisser vide pour ne jamais expirer",
"Click the button below to choose a new password:": "Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe :",
"Code Verification" : "Vérification du code",
"Code": "Code",
"Configuration": "Configuration",
"Configure your Linphone application": "Configurer votre application Linphone",
"Configure": "Configurer",
"Confirm email": "Confirmer l'email",
"Confirm password": "Confirmer le mot de passe",
"Confirm your registration": "Confirmez votre inscription",
"Confirmed registration text": "Texte de confirmation d'inscription",
"Connect to your account":"Connexion à votre compte",
"Connection": "Connexion",
"Contacts List": "Liste de Contacts",
"Contacts Lists": "Listes de Contacts",
"Contacts": "Contacts",
"Configure": "Configurer",
"Copyright text": "Texte droits d'auteurs",
"Country code": "Code du pays",
"Create": "Créer",
@ -69,10 +77,11 @@
"Dictionary": "Dictionnaire",
"Display name": "Nom d'affichage",
"Domain": "Domaine",
"Dont have the app yet?": "Vous navez pas encore lapplication ?",
"Download Linphone" : "Télécharger Linphone",
"Edit": "Éditer",
"By email": "Inscription par email",
"Email": "Email",
"Email Server": "Serveur Mail",
"Email": "Email",
"Empty": "Vide",
"Enable the web interface": "Activer l'interface web",
"Enabled": "Activé",
@ -80,31 +89,36 @@
"Enter the code you received below": "Saisissez le code reçu ci-dessous",
"Errors": "Erreurs",
"Expiration": "Expiration",
"Expired Space": "Espace expiré",
"Expired": "Expiré",
"Export": "Exporter",
"External Account": "Compte Externe",
"Expired Space": "Espace expiré",
"Features": "Fonctionnalités",
"Fill to change": "Remplir pour changer",
"Fill the related columns if you want to add an external account as well": "Remplissez également les colonnes suivantes si vous souhaitez ajouter un compte externe",
"Fill to change": "Remplir pour changer",
"From": "Depuis",
"Hello":"Bonjour",
"Host": "Hôte",
"I accept the Privacy Policy": "J'accepte la Politique de Confidentialité",
"I accept the Terms and Conditions": "J'accepte les Conditions Générales",
"I would like to subscribe to the newsletter": "Je voudrais m'inscrire à la newsletter",
"I'm not a robot": "Je ne suis pas un robot",
"Identifier": "Identifiant",
"If you have any questions or need assistance, feel free to contact our support team.":"Si vous avez des questions ou besoin dassistance, notre équipe reste à votre disposition",
"Import": "Importer",
"In :days days": "Dans :days jours",
"In ini format, will complete the other settings": "Au format ini, complètera les autres paramètres",
"In ini format, will complete the other settings.": "Au format ini, complètera les autres paramètres.",
"In lowercase letters": "En minuscules",
"Incorrect username or password": "Nom d'utilisateur ou mot de passe incorrect",
"Information": "Informations",
"Instant Messaging": "Messagerie Instantanée",
"Integration": "Intégration",
"Intercom features": "Fonctionnalités d'interphonie",
"It might actually disable this page, be careful": "Cette page pourrait être désactivée, faites attention",
"Key": "Clef",
"Last used": "Dernière utilisation",
"Leave empty to create a root Space.": "Laisser vide si vous souhaitez créer un Espace à la racine",
"Login to my account":"Connexion à mon compte",
"Login using a QRCode": "S'authentifier avec un QRCode",
"Login": "Authentification",
"Manage": "Gérer",
@ -115,56 +129,56 @@
"My Account": "Mon Compte",
"My Space": "Mon Espace",
"Name": "Nom",
"Never": "Jamais",
"Need help?" : "Besoin daide ?",
"Never expire": "N'expire jamais",
"Never": "Jamais",
"New Admin": "Nouvel Admin",
"New user": "Nouvel utilisateur",
"New newsletter subscription": "Nouvelle inscription à votre newsletter",
"New Space": "Nouvel Espace",
"New user": "Nouvel utilisateur",
"Newsletter registration email address": "Addresse email d'inscription à la liste de diffusion",
"Newsletter registration confirmed": "Confirmation de l'inscription à la newsletter",
"Next": "Suivant",
"No account yet?": "Pas encore de compte ?",
"No email yet": "Pas d'email pour le moment",
"No limit": "Sans limite",
"No phone yet": "Pas de téléphone pour le moment",
"Number of minutes to expire the key after the last request.": "Nombre de minutes avant l'expiration de la clef après son dernier usage.",
"Open the app": "Ouvrir l'application",
"Other information": "Autres informations",
"Outbound proxy": "Outbound proxy",
"Password": "Mot de passe",
"Your password" : "Votre mot de passe",
"Phone Countries": "Numéros Internationaux",
"Phone number": "Numéro de téléphone",
"By phone": "Par téléphone",
"Phone registration": "Inscription par téléphone",
"Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.",
"Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.",
"Protocol": "Protocole",
"Provisioning of your device": "Déploiement sur votre appareil",
"Provisioning tokens": "Jetons de déploiement",
"Provisioning": "Déploiement",
"Provisioning of your device": "Déploiement sur votre appareil",
"Proxy/registrar address":"Adresse Proxy/registrar",
"Public registration": "Inscription publiques",
"QR Code scanning": "Scan de QR Code",
"Realm": "Royaume",
"Recover your account using your email": "Récupérer votre compte avec votre email",
"Use the mobile app to recover your account using your phone number": "Utilisez l'application mobile pour récupérer votre compte avec votre numéro de téléphone",
"Username or Phone": "Nom d'utilisateur ou téléphone",
"Register": "Inscription",
"Registrar": "Registrar",
"Registration introduction": "Présentation lors de l'inscription",
"Registration confirmed": "Confirmation de l'inscription",
"Remote provisioning": "configuration distante",
"Registration introduction": "Présentation lors de l'inscription",
"Remote provisioning": "Configuration distante",
"Remove": "Remove",
"Renew": "Renouveller",
"Requests": "Requêtes",
"Resend": "Renvoyer",
"Reset my password":"Réinitialiser mon mot de passe",
"Reset password emails": "Email de réinitialisation de mot de passe",
"Reset password": "Réinitialiser le mot de passe",
"Reset your password": "Réinitialiser votre mot de passe",
"Reset": "Réinitialiser",
"Role": "Rôle",
"Scan the following QR Code using an authenticated device and wait a few seconds.": "Scanner le QR Code avec un appareil authentifié et attendez quelques secondes",
"Search": "Rechercher",
"Search by username":"Rechercher par nom d'utilisateur",
"Search": "Rechercher",
"Select a contacts list": "Sélectionner une liste de contact",
"Select a domain": "Sélectionner un domaine",
"Select a file": "Choisir un fichier",
@ -173,28 +187,35 @@
"Send": "Envoyer",
"Settings": "Paramètres",
"Show usernames only": "Afficher uniquement les noms d'utilisateur",
"SIP address":"Adresse SIP",
"Sip Adress": "Adresse SIP",
"SIP Domain": "Domaine SIP",
"Space": "Espace",
"Space is expiring in :days days": "Votre Espace expire dans %d jours",
"Spaces": "Espaces",
"Statistics": "Statistiques",
"Subdomain": "Sous-domaine",
"Super Admin": "Super Admin",
"Super Space": "Super Espace",
"Thank you for registering on :space.":"Merci de vous être inscrit sur :space.",
"Thanks for the validation": "Nous vous remercions pour la validation",
"The :attribute should not be a phone number": "Le champ :attribute ne peut pas être un numéro de téléphone",
"The account doesn't exists": "Le compte n'existe pas",
"The code has expired": "Le code a expiré",
"The code is not valid": "Le code n'est pas valide",
"We will send you a verification code to recover your account.": "Nous vous enverrons un code de vérification pour récupérer votre compte.",
"The contact doesn't exists": "Le contact n'existe pas",
"The file must be in CSV following this template": "Le fichier doit être au format CSV et respecter le modèle suivant",
"The file MUST be in CSV following this template": "Le fichier doit être au format CSV et respecter le modèle suivant",
"The first line contains the labels": "La premières ligne contient les étiquettes",
"The following email address wants to register to the mailing list:":"Ladresse e-mail suivante souhaite sinscrire à la liste de diffusion :",
"The link can only be visited once": "Le lien ne peut être utilisé qu'une fois",
"Third-party SIP account": "Compte SIP tiers",
"This code is valid for :minutes minutes.": "Ce code est valable pendant :minutes minutes.",
"This link is not available anymore.": "Ce lien n'est plus disponible.",
"This link will be available for :hours hours.": "Ce lien restera disponible pour :hours heures.",
"This link will expire in :hour hours.":"Ce lien expirera dans :hour heures.",
"To complete your registration, please enter the verification code below in the registration form:":"Pour finaliser votre inscription, veuillez saisir le code de vérification ci-dessous dans le formulaire d'inscription :",
"To ensure the continuity of your services (SIP calls, user accounts, configurations, etc.), we recommend renewing or updating your subscription before the expiration date.":"Afin dassurer la continuité de vos services (appels SIP, comptes utilisateurs, configurations, etc.), nous vous invitons à renouveler ou mettre à jour votre abonnement avant cette date.",
"To proceed, please enter the verification code below:":"Veuillez saisir le code de vérification ci-dessous pour poursuivre :",
"To start using your account, click the button below:":"Pour commencer à utiliser votre compte, cliquez sur le bouton ci-dessous :",
"To": "À",
"Transport": "Transport",
"Types": "Types",
@ -202,22 +223,38 @@
"Update": "Mettre à jour",
"Updated on": "Mis à jour le",
"Updated": "Mise à jour",
"Use ; to comment, key=\"value\" to declare a complex string.": "Utilisez ; pour commenter, clef=\"valeur\" pour déclarer une chaine complexe.",
"Use the mobile app to recover your account using your phone number": "Utilisez l'application mobile pour récupérer votre compte avec votre numéro de téléphone",
"Used on": "Utilisé le",
"User": "Utilisateur",
"Users": "Utilisateurs",
"Username or Phone": "Nom d'utilisateur ou téléphone",
"Username": "Nom d'utilisateur",
"Users": "Utilisateurs",
"Value": "Valeur",
"Verify": "Vérifier",
"Via": "Via",
"Visit our user guide" : "Consulter notre guide utilisateur",
"We are pleased to inform you that your account has been successfully created.":"Nous avons le plaisir de vous informer que votre compte a été créé avec succès.",
"We inform you that this space will expire on :date, in accordance with the expiration date defined in your subscription.":"Nous vous informons que l'espace expira le :date, conformément à la date dexpiration définie dans votre abonnement.",
"We received a request to recover your account on :space": "Nous avons reçu une demande de récupération de votre compte sur :space",
"We received a request to reset your password for your account on :space.":"Nous avons reçu une demande de réinitialisation de mot de passe pour votre compte sur :space",
"We will send you a verification code to recover your account.": "Nous vous enverrons un code de vérification pour récupérer votre compte.",
"Week": "Semaine",
"Welcome on :app_name": "Bienvenue sur :app_name",
"Incorrect username or password": "Nom d'utilisateur ou mot de passe incorrect",
"Welcome to :space: Start using your account today": "Bienvenue sur :space : commencez à utiliser votre compte dès maintenant",
"Welcome to :space":"Bienvenue sur :space",
"Year": "Année",
"You already have an account?": "Vous avez déjà un compte ?",
"You are going to permanently delete the following element. Please confirm your action.": "Vous allez supprimer l'élément suivant. Veuillez confirmer votre action.",
"You are going to permanently delete your account. Please enter your complete SIP address to confirm.": "Vous allez détruire défitivement votre compte. Veuillez entrer votre addresse SIP complète pour confirmer.",
"You are one of the administrators of the :space space configured on our service.":"Vous êtes lun des administrateurs de lespace :space configuré sur notre service.",
"You can also connect your account to the mobile app by scanning the QR code with the app":"Vous pouvez également connecter votre compte à lapplication mobile en scannant le QR code avec lapplication.",
"You can now configure this account on any SIP-compatible application using the following parameters:":"Vous pouvez désormais configurer ce compte sur toute application compatible SIP en utilisant les paramètres suivants :",
"You can now continue your registration process in the application": "Vous pouvez maintenant continuer le processus d'inscription dans l'application",
"You didn't receive the code?": "Vous n'avez pas reçu le code ?",
"Your account recovery code":"Votre code de récupération de compte",
"Your password was updated properly.": "Votre mot de passe a été mis à jour.",
"Your password" : "Votre mot de passe",
"Your space :space is expiring in :count days": "Votre espace :space expire dans :count jours",
"Your space has expired. Access to your interface is now disabled, and your users can no longer benefit from the service. To reactivate your space, please contact your account manager.": "Votre espace est arrivé à expiration. Laccès à votre interface est désormais désactivé, et vos utilisateurs ne peuvent plus bénéficier du service. Pour réactiver votre espace, veuillez contacter votre responsable de compte."
}

View file

@ -1,4 +1,4 @@
Username,Password,Role,Status,Phone,Email,External Username,External Domain,External Password,External Realm, External Registrar,External Outbound Proxy,External Encrypted,External Protocol
Username,Password,Role,Status,Phone,Email,External Username,External Domain,External Password,External Realm, External Registrar,External Outbound Proxy,External Protocol
john,number9,user,active,+12341234,john@lennon.com,extjohn,ext.lennon.com,123ext,,,,UDP
paul,a_day_in_the_life,admin,active,,paul@apple.com,,,,,,,
ringo,allUneedIsL3ve,user,inactive,+123456,ringo@star.co.uk,extringo,ext.star.co.uk,123456,another.realm,,,UDP
1 Username,Password,Role,Status,Phone,Email,External Username,External Domain,External Password,External Realm, External Registrar,External Outbound Proxy,External Encrypted,External Protocol Username Password Role Status Phone Email External Username External Domain External Password External Realm External Registrar External Outbound Proxy External Protocol
2 john,number9,user,active,+12341234,john@lennon.com,extjohn,ext.lennon.com,123ext,,,,UDP john number9 user active +12341234 john@lennon.com extjohn ext.lennon.com 123ext UDP
3 paul,a_day_in_the_life,admin,active,,paul@apple.com,,,,,,, paul a_day_in_the_life admin active paul@apple.com
4 ringo,allUneedIsL3ve,user,inactive,+123456,ringo@star.co.uk,extringo,ext.star.co.uk,123456,another.realm,,,UDP ringo allUneedIsL3ve user inactive +123456 ringo@star.co.uk extringo ext.star.co.uk 123456 another.realm UDP

View file

@ -46,6 +46,7 @@ p .btn.oppose {
pointer-events: none;
}
.btn:focus-visible,
.btn:hover {
background-color: var(--main-6);
border-color: var(--main-6);
@ -228,14 +229,18 @@ form div.search:after {
}
form div span.supporting {
line-height: 2rem;
font-size: 1.25rem;
color: var(--grey-4);
display: block;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
form div span.supporting,
form div span.supporting a {
line-height: 2rem;
font-size: 1.25rem;
}
form div input[disabled],
form div textarea[disabled] {
border-color: var(--grey-4);
@ -307,8 +312,8 @@ form div select:hover {
border-color: var(--second-4);
}
form div input:focus-visible,
form div input:active,
form div input:not(.btn):focus-visible,
form div input:not(.btn):active,
form div textarea:focus-visible,
form div textarea:active {
color: var(--main-5);

View file

@ -76,6 +76,16 @@ body {
--color-purple: rgba(151, 71, 255, 1);
}
#wizard h3 {
margin-bottom: 3rem;
}
#wizard .btn {
display: block;
text-align: center;
margin: 1rem 0;
}
body.show_menu {
max-height: 100vh;
overflow: hidden;
@ -154,6 +164,7 @@ code {
p>a:not(.btn),
ul:not(.tabs)>li>a,
table tr td a:not(.btn):hover,
form div span.supporting a,
label>a {
text-decoration: underline;
color: var(--main-5);

View file

@ -8,7 +8,7 @@
@section('content')
<div class="large">
<h2><i class="ph">key</i>API Key</h2>
<h2><i class="ph ph-key"></i>API Key</h2>
<p>You can generate an API key and use it to request the different API endpoints, <a href="{{ route('api') }}">check
the related API documentation</a> to know how to use that key.</p>
@ -21,7 +21,7 @@
<div>
<input type="text" readonly value="{{ $account->apiKey->key }}">
<label>Key</label>
<small>Can only be used from the following ip: {{ $account->apiKey->ip }} | {{ __('Requests ')}} {{ $account->apiKey->requests }}</small>
<small>Can only be used from the following ip: {{ $account->apiKey->ip }} | {{ __('Requests ') }} {{ $account->apiKey->requests }}</small>
</div>
</form>
@endif

View file

@ -2,13 +2,13 @@
@section('content')
<header>
<h1><i class="ph">gauge</i> {{ __('My Account') }}</h1>
<h1><i class="ph ph-gauge"></i> {{ __('My Account') }}</h1>
</header>
<div class="card">
<h3><i class="ph">hand-waving</i> {{ __('Welcome on :app_name' , ['app_name' => space()->name]) }} </h3>
<h3><i class="ph ph-hand-waving"></i> {{ __('Welcome on :app_name' , ['app_name' => space()->name]) }} </h3>
<p>
<i class="ph">envelope</i>
<i class="ph ph-envelope"></i>
@if (!empty($account->email))
{{ $account->email }}
@else
@ -19,7 +19,7 @@
@if (space()->phone_registration)
<p>
<i class="ph">phone</i>
<i class="ph ph-phone"></i>
@if (!empty($account->phone))
{{ $account->phone }}
@else
@ -30,14 +30,14 @@
@endif
<p>
<i class="ph">devices</i>
<i class="ph ph-devices"></i>
{{ __('Devices') }}
<a href="{{ route('account.device.index') }}">
{{ __('Manage') }}
</a>
</p>
<p>
<i class="ph">lock</i>
<i class="ph ph-lock"></i>
{{ __('Password') }}
<a href="{{ route('account.password.show') }}">
@if ($account->passwords()->count() > 0)
@ -49,7 +49,7 @@
</p>
<p>
<i class="ph">key</i>
<i class="ph ph-key"></i>
{{ __('API Key') }}
<a href="{{ route('account.api_keys.show') }}">
{{ __('Manage') }}
@ -57,7 +57,7 @@
</p>
<p>
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
{{ __('My Account') }}
<a href="{{ route('account.delete') }}">
{{ __('Delete') }}
@ -66,18 +66,18 @@
</div>
<div class="card">
<h3><i class="ph">person</i> {{ __('Information') }}</h3>
<h3><i class="ph ph-person"></i> {{ __('Information') }}</h3>
<p><i class="ph">envelope</i> {{ __('SIP Adress') }}: sip:{{ $account->identifier }}</p>
<p><i class="ph">user</i> {{ __('Username') }}: {{ $account->username }}</p>
<p><i class="ph">globe-hemisphere-west</i> {{ __('Domain') }}: {{ $account->domain }}</p>
<p><i class="ph ph-envelope"></i> {{ __('SIP Adress') }}: sip:{{ $account->identifier }}</p>
<p><i class="ph ph-user"></i> {{ __('Username') }}: {{ $account->username }}</p>
<p><i class="ph ph-globe-hemisphere-west"></i> {{ __('Domain') }}: {{ $account->domain }}</p>
@if (!empty(space()?->account_proxy_registrar_address))
<p><i class="ph">hard-drive</i> Proxy/registrar address: sip:{{ space()?->account_proxy_registrar_address }}
<p><i class="ph ph-hard-drive"></i> Proxy/registrar address: sip:{{ space()?->account_proxy_registrar_address }}
</p>
@endif
@if (!empty(config('app.transport_protocol_text')))
<p><i class="ph">sliders</i> {{ __('Transport') }}: {{ config('app.transport_protocol_text') }} </p>
<p><i class="ph ph-sliders"></i> {{ __('Transport') }}: {{ config('app.transport_protocol_text') }} </p>
@endif
<!--<h3 class="mt-3">Automatic authentication</h3>

View file

@ -6,7 +6,7 @@
@section('content')
<header>
<h1><i class="ph">trash</i> Delete my account</h1>
<h1><i class="ph ph-trash"></i> Delete my account</h1>
<a href="{{ route('account.dashboard') }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">

View file

@ -7,7 +7,7 @@
@section('content')
<header>
<h1><i class="ph">devices</i> {{ __('Devices') }}</h1>
<h1><i class="ph ph-devices"></i> {{ __('Devices') }}</h1>
</header>
<table>

View file

@ -3,7 +3,7 @@
@section('content')
<section>
<h1>
<i class="ph">envelope</i>
<i class="ph ph-envelope"></i>
{{ __('Change your email') }}
</h1>

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1 style="margin-bottom: 4rem;"><i class="ph">user-circle</i> {{ __('Code Verification') }}</h1>
<h1 style="margin-bottom: 4rem;"><i class="ph ph-user-circle"></i> {{ __('Code Verification') }}</h1>
<form method="POST" action="{{ route('account.email.update') }}" accept-charset="UTF-8">
@csrf
@ -28,8 +28,7 @@
<div class="large" style="margin-top: 2rem;">
<p>
{{ __('You didn't receive the code?') }}
{{ __("You didn't receive the code?") }}
<a class="btn secondary" href="{{ route('account.email.change') }}">{{ __('Resend') }}</a>
</p>
</div>

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1 style="margin-bottom: 3rem;"><i class="ph">hand-waving</i> {{ __('Welcome on :app_name' , ['app_name' => space()->name]) }}</h1>
<h1 style="margin-bottom: 3rem;"><i class="ph ph-hand-waving"></i> {{ __('Welcome on :app_name' , ['app_name' => space()->name]) }}</h1>
@if (space()->intro_registration_text)
@parsedown(space()->intro_registration_text)

View file

@ -7,9 +7,9 @@
@section('content')
<header>
@if ($account->passwords()->count() > 0)
<h1><i class="ph">lock</i> {{ __('Edit') }}</h1>
<h1><i class="ph ph-lock"></i> {{ __('Edit') }}</h1>
@else
<h1><i class="ph">lock</i> {{ __('Create') }}</h1>
<h1><i class="ph ph-lock"></i> {{ __('Create') }}</h1>
@endif
<a href="{{ route('account.dashboard') }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
@ -22,6 +22,7 @@
<div>
<input type="password" name="password" required>
<label for="password">{{ __('Password') }}</label>
@include('parts.errors', ['name' => 'password'])
</div>
<div>
<input type="password" name="password_confirmation" required>

View file

@ -3,12 +3,12 @@
@section('content')
<section>
<header>
<h1><i class="ph">lock</i> {{ __('Reset password') }}</h1>
<h1><i class="ph ph-lock"></i> {{ __('Reset password') }}</h1>
</header>
<p>{{ __('Your password was updated properly.') }}</p>
<p>
<a class="btn" href="{{ route('account.login')}}">{{ __('Authenticate') }}</a>
<a class="btn" href="{{ route('account.login') }}">{{ __('Authenticate') }}</a>
</p>
</section>

View file

@ -3,7 +3,7 @@
@section('content')
<section>
<header>
<h1><i class="ph">lock</i> {{ __('Reset') }}</h1>
<h1><i class="ph ph-lock"></i> {{ __('Reset') }}</h1>
</header>
@if ($token->offed())

View file

@ -3,7 +3,7 @@
@section('content')
<section>
<h1>
<i class="ph">phone</i>
<i class="ph ph-phone"></i>
{{ __('Change your phone number') }}
</h1>

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1><i class="ph">user-circle</i> {{ __('Change your phone number') }}</h1>
<h1><i class="ph ph-user-circle"></i> {{ __('Change your phone number') }}</h1>
<form method="POST" action="{{ route('account.phone.update') }}" accept-charset="UTF-8">
@csrf
@ -28,7 +28,7 @@
<div class="large" style="margin-top: 2rem;">
<p>
{{ __("You didn't receive the code?"") }}
{{ __("You didn't receive the code?") }}
<a class="btn secondary" href="{{ route('account.phone.change') }}">{{ __('Resend') }}</a>
</p>
</div>

View file

@ -3,7 +3,7 @@
@section('content')
<section>
<h1><i class="ph">user-circle</i> {{ __('Account recovery') }}</h1>
<h1><i class="ph ph-user-circle"></i> {{ __('Account recovery') }}</h1>
<form method="POST" action="{{ route('account.recovery.confirm') }}" accept-charset="UTF-8">
@csrf

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1><i class="ph">user-circle</i> {{ __('Account recovery') }}</h1>
<h1><i class="ph ph-user-circle"></i> {{ __('Account recovery') }}</h1>
<div>
<form method="POST" action="{{ route('account.recovery.send') }}" accept-charset="UTF-8">
@csrf
@ -53,7 +53,7 @@
@endif
<div class="large">
<input class="btn oppose" type="submit" value="{{ __('Send')}}">
<input class="btn oppose" type="submit" value="{{ __('Send') }}">
</div>
</form>
</div>

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1><i class="ph">user-circle</i> {{ __('Register') }}</h1>
<h1><i class="ph ph-user-circle"></i> {{ __('Register') }}</h1>
<p style="margin-bottom: 2rem;">
{{ __('You already have an account?') }}
<a class="btn secondary" href="{{ route('account.login') }}">{{ __('Login') }}</a>

View file

@ -2,7 +2,7 @@
@section('content')
<section>
<h1><i class="ph">user-circle</i> {{ __('Register') }}</h1>
<h1><i class="ph ph-user-circle"></i> {{ __('Register') }}</h1>
<p style="margin-bottom: 2rem;">
{{ __('You already have an account?') }}
<a class="btn secondary" href="{{ route('account.login') }}">{{ __('Login') }}</a>

View file

@ -9,7 +9,7 @@
@section('content')
<header>
<h1><i class="ph">list</i> {{ $account->identifier }}</h1>
<h1><i class="ph ph-list"></i> {{ $account->identifier }}</h1>
</header>
@include('admin.account.parts.tabs')
@ -114,7 +114,7 @@
<thead>
<tr>
<th>{{ __('Created') }}</th>
<th>{{ __('Via') }} <i class="ph">phone</i>/<i class="ph">envelope</i></th>
<th>{{ __('Via') }} <i class="ph ph-phone"></i><i class="ph ph-envelope"></i></th>
<th>{{ __('Used on') }}</th>
</tr>
</thead>
@ -129,9 +129,9 @@
</td>
<td>
@if ($recoveryCode->phone)
<i class="ph">phone</i> {{ $recoveryCode->phone }}
<i class="ph ph-phone"></i> {{ $recoveryCode->phone }}
@elseif($recoveryCode->email)
<i class="ph">envelope</i> {{ $recoveryCode->email }}
<i class="ph ph-envelope"></i> {{ $recoveryCode->email }}
@endif
</td>
<td>

View file

@ -8,7 +8,7 @@
@section('content')
<header>
<h1><i class="ph">user-plus</i> {{ __('Add contact') }}</h1>
<h1><i class="ph ph-user-plus"></i> {{ __('Add contact') }}</h1>
<a href="{{ route('admin.account.edit', $account->id) }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="add_contact" class="btn" type="submit" value="Add">
</header>

View file

@ -12,15 +12,15 @@
@section('content')
<h2>{{ __('Delete') }}</h2>
<div>
<p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p>
<p><b>{{ $contact->identifier }}</b></p>
</div>
<form method="POST" action="{{ route('admin.account.contact.destroy', [$account]) }}" accept-charset="UTF-8">
@csrf
@method('delete')
<div>
<p>{{ $device->user_agent }}</p>
<p><b>{{ $contact->identifier }}</b></p>
</div>
<input name="account_id" type="hidden" value="{{ $account->id }}">
<input name="contact_id" type="hidden" value="{{ $contact->id }}">
<div>

View file

@ -8,12 +8,12 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
<h1><i class="ph ph-users"></i> {{ $account->identifier }}</h1>
</header>
@include('admin.account.parts.tabs')
<a class="btn small oppose" href="{{ route('admin.account.contact.create', $account) }}">
<i class="ph">plus</i> {{ __('Add') }}
<i class="ph ph-plus"></i> {{ __('Add') }}
</a>
<h3>
{{ __('Contacts') }}
@ -32,7 +32,7 @@
</td>
<td class="actions">
<a type="button" class="btn small tertiary" href="{{ route('admin.account.contact.delete', [$account, $contact->id]) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -55,7 +55,7 @@
</td>
<td class="actions">
<a type="button" class="btn small tertiary" href="{{ route('admin.account.contacts_lists.detach', ['account_id' => $account->id, 'contacts_list_id' => $contactsList->id]) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -67,7 +67,7 @@
<div class="card">
<div class="grid">
<div>
<h4><i class="ph">plus</i> {{ __('Add') }}</h4>
<h4><i class="ph ph-plus"></i> {{ __('Add') }}</h4>
<p>{{ __('Add existing contacts lists to display them in the user applications.') }}</p>
</div>
<div>

View file

@ -13,7 +13,7 @@
@section('content')
@if ($account->id)
<header>
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
<h1><i class="ph ph-users"></i> {{ $account->identifier }}</h1>
</header>
@if ($account->updated_at)
<p title="{{ $account->updated_at }}">{{ __('Updated on') }} {{ $account->updated_at->format('d/m/Y') }}
@ -21,7 +21,7 @@
@include('admin.account.parts.tabs')
@else
<header>
<h1><i class="ph">users</i> {{ __('New user') }}</h1>
<h1><i class="ph ph-users"></i> {{ __('New user') }}</h1>
<a href="{{ route('admin.account.index') }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
</header>
@endif
@ -42,7 +42,7 @@
<div class="select">
<select name="domain" @if (auth()->user()?->superAdmin) required @else disabled @endif>
@foreach ($domains as $space)
<option value="{{ $space->domain }}" @if ($account->domain == $space->domain) selected="selected" @endif>
<option value="{{ $space->domain }}" @if ($account->domain == $space->domain || old('domain') == $space->domain) selected="selected" @endif>
{{ $space->domain }}</option>
@endforeach
</select>

View file

@ -8,7 +8,7 @@
@section('content')
<header>
<h1><i class="ph">trash</i> {{ __('Delete') }}</h1>
<h1><i class="ph ph-trash"></i> {{ __('Delete') }}</h1>
<a href="{{ route('admin.account.show', $account->id) }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">

View file

@ -3,9 +3,6 @@
@section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index')
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item">
<a href="{{ route('admin.account.device.index', $account) }}">Devices</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection

View file

@ -9,7 +9,7 @@
@section('content')
<header>
<h1><i class="ph">trash</i> {{ __('Delete') }}</h1>
<h1><i class="ph ph-trash"></i> {{ __('Delete') }}</h1>
<a href="{{ route('admin.account.external.show', ['account' => $account]) }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">

View file

@ -8,10 +8,10 @@
@section('content')
<header>
<h1><i class="ph">user-circle-dashed</i> {{ __('External Account') }}</h1>
<h1><i class="ph ph-user-circle-dashed"></i> {{ __('External Account') }}</h1>
@if($externalAccount->id)
<a class="btn secondary oppose" href="{{ route('admin.account.external.delete', $account->id) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
{{ __('Delete') }}
</a>
@endif

View file

@ -7,7 +7,7 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ __('Import') }}</h1>
<h1><i class="ph ph-users"></i> {{ __('Import') }}</h1>
<a href="{{ route('admin.account.index') }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<a href="#" onclick="history.back()" class="btn secondary">Previous</a>
@ -18,7 +18,7 @@
<input name="domain" type="hidden" value="{{ $domain }}">
<a type="submit"
class="btn @if ($errors->isNotEmpty()) disabled @endif" onclick="document.querySelector('form[name=handle]').submit()">
<i class="ph">download-simple</i>
<i class="ph ph-download-simple"></i>
{{ __('Import') }}
</a>
</form>

View file

@ -7,7 +7,7 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ __('Import') }}</h1>
<h1><i class="ph ph-users"></i> {{ __('Import') }}</h1>
<a href="{{ route('admin.account.index') }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="import" class="btn" type="submit" value="{{ __('Next') }}">
</header>
@ -18,7 +18,7 @@
<li>{{ __('Import') }}</li>
</ol>
<p>{{ __('The file must be in CSV following this template') }}: <a href="{{ route('account.home') }}/accounts_example.csv">example_template.csv</a></p>
<p>{{ __('The file MUST be in CSV following this template') }}: <a href="{{ route('account.home') }}/accounts_example.csv">example_template.csv</a></p>
<h4>{{ __('User') }}</h4>
<p>{{ __('The first line contains the labels') }}</p>

View file

@ -2,22 +2,22 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ __('Users') }}</h1>
<h1><i class="ph ph-users"></i> {{ __('Users') }}</h1>
@if ($space)
<p>{{ $accounts->count()}} / @if ($space->max_accounts > 0){{ $space->max_accounts }} @else <i class="ph">infinity</i>@endif</p>
<p>{{ $accounts->count() }} / @if ($space->max_accounts > 0){{ $space->max_accounts }} @else <i class="ph ph-infinity"></i>@endif</p>
@endif
<a class="btn secondary oppose" href="{{ route('admin.account.import.create') }}">
<i class="ph">upload-simple</i>
<i class="ph ph-upload-simple"></i>
{{ __('Import') }}
</a>
@if (space()?->intercom_features)
<a class="btn secondary" href="{{ route('admin.account.type.index') }}">
<i class="ph">shapes</i>
<i class="ph ph-shapes"></i>
{{ __('Types') }}
</a>
@endif
<a class="btn" href="{{ route('admin.account.create') }}">
<i class="ph">user-plus</i>
<a class="btn" @if ($space && $space->isFull())disabled @endif href="{{ route('admin.account.create') }}">
<i class="ph ph-user-plus"></i>
{{ __('New user') }}
</a>
</header>
@ -53,7 +53,9 @@
</div>
<div class="oppose">
<a href="{{ route('admin.account.index') }}" type="reset" class="btn tertiary">{{ __('Reset') }}</a>
<button type="submit" class="btn">{{ __('Search') }}</button>
<button type="submit" class="btn">
<i class="ph ph-magnifying-glass"></i>
</button>
</div>
</form>
</div>

View file

@ -1,5 +1,5 @@
@if ($account->activated)
<span class="badge badge-success" title="{{ __('Activated') }}"><i class="ph">check</i></span>
<span class="badge badge-success" title="{{ __('Activated') }}"><i class="ph ph-check"></i></span>
@endif
@if ($account->superAdmin)
<span class="badge badge-error" title="{{ __('Super Admin') }}">Super Adm.</span>
@ -7,5 +7,5 @@
<span class="badge badge-primary" title="{{ __('Admin') }}">Adm.</span>
@endif
@if ($account->blocked)
<span class="badge badge-error" title="{{ __('Blocked') }}"><i class="ph">prohibit</i></span>
<span class="badge badge-error" title="{{ __('Blocked') }}"><i class="ph ph-prohibit"></i></span>
@endif

View file

@ -3,13 +3,13 @@
@section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index')
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Reset password') }}</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Provisioning') }}</li>
@endsection
@section('content')
<header>
<h1><i class="ph">envelope</i> {{ __('Reset password') }}</h1>
<h1><i class="ph ph-envelope"></i> {{ __('Provisioning') }}</h1>
</header>
<p>{{ __('An email will be sent to :email with a QR Code and provisioning link.', ['email' => $account->email]) }}</p>
@ -20,7 +20,7 @@
<p>
<a class="btn oppose" href="{{ route('admin.account.provisioning_email.send', $account) }}">
<i class="ph">paper-plane-right</i> {{ __('Send') }}
<i class="ph ph-paper-plane-right"></i> {{ __('Send') }}
</a>
</p>

View file

@ -9,7 +9,7 @@
@section('content')
<header>
<h1><i class="ph">envelope</i> {{ __('Reset password') }}</h1>
<h1><i class="ph ph-envelope"></i> {{ __('Reset password') }}</h1>
</header>
<p>{{ __('An email will be sent to :email with a unique link allowing the user to reset its password.', ['email' => $account->email]) }}</p>
@ -20,7 +20,7 @@
<p>
<a class="btn oppose" href="{{ route('admin.account.reset_password_email.send', $account) }}">
<i class="ph">paper-plane-right</i> {{ __('Send') }}
<i class="ph ph-paper-plane-right"></i> {{ __('Send') }}
</a>
</p>

View file

@ -7,14 +7,14 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
<h1><i class="ph ph-users"></i> {{ $account->identifier }}</h1>
</header>
@include('admin.account.parts.tabs')
<div class="grid">
<div class="card">
<a class="btn small oppose" href="{{ route('admin.account.edit', $account) }}">
<i class="ph">pencil</i>
<i class="ph ph-pencil"></i>
{{ __('Edit') }}
</a>
<h3>
@ -24,19 +24,19 @@
{{ __('Information') }}
</h3>
<p><i class="ph">user</i> {{ __('SIP Adress') }}: sip:{{ $account->identifier }}</p>
<p><i class="ph ph-user"></i> {{ __('SIP Adress') }}: sip:{{ $account->identifier }}</p>
@if ($account->email)
<p><i class="ph">envelope</i> {{ __('Email') }}: {{ $account->email }}</p>
<p><i class="ph ph-envelope"></i> {{ __('Email') }}: {{ $account->email }}</p>
@endif
@if ($account->phone)
<p><i class="ph">phone</i> {{ __('Phone') }}: {{ $account->phone }}</p>
<p><i class="ph ph-phone"></i> {{ __('Phone') }}: {{ $account->phone }}</p>
@endif
@if ($account->passwords()->count() > 0)
<p><i class="ph">password</i> {{ __('Password') }}: **********</p>
<p><i class="ph ph-password"></i> {{ __('Password') }}: **********</p>
@endif
<p>
<i class="ph">globe-hemisphere-west</i>
<i class="ph ph-globe-hemisphere-west"></i>
{{ __('Space') }}: <a href="{{ route('admin.spaces.show', $account->space->id) }}">{{ $account->domain }}</a>
</p>
<p>
@ -53,7 +53,7 @@
<td>{{ __('Send an email to the user to reset the password') }}</td>
<td class="actions">
<a class="btn secondary small" href="{{ route('admin.account.reset_password_email.create', $account) }}">
<i class="ph">paper-plane-right</i>
<i class="ph ph-paper-plane-right"></i>
</a>
</td>
</tr>
@ -61,7 +61,7 @@
<td>{{ __('Send an email to the user with provisioning information') }}</td>
<td class="actions">
<a class="btn secondary small" href="{{ route('admin.account.provisioning_email.create', $account) }}">
<i class="ph">paper-plane-right</i>
<i class="ph ph-paper-plane-right"></i>
</a>
</td>
</tr>
@ -71,7 +71,7 @@
</td>
<td class="actions">
<a class="btn tertiary small" href="{{ route('admin.account.delete', $account->id) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -81,7 +81,7 @@
<div class="card">
<a class="btn small oppose" href="{{ route('admin.account.external.show', $account) }}">
<i class="ph">pencil</i>
<i class="ph ph-pencil"></i>
@if ($account->external){{ __('Edit') }}@else{{ __('Create') }}@endif
</a>
<h3>
@ -89,13 +89,13 @@
</h3>
@if ($account->external)
@if ($account->external->username)
<p><i class="ph">user</i> {{ __('Usernale') }}: {{ $account->external->username }}</p>
<p><i class="ph ph-user"></i> {{ __('Username') }}: {{ $account->external->username }}</p>
@endif
@if ($account->external->domain)
<p><i class="ph">hard-drive</i> {{ __('Domain') }}: {{ $account->external->domain }}</p>
<p><i class="ph ph-hard-drive"></i> {{ __('Domain') }}: {{ $account->external->domain }}</p>
@endif
@if ($account->external->password)
<p><i class="ph">password</i> {{ __('Password') }}: **********</p>
<p><i class="ph ph-password"></i> {{ __('Password') }}: **********</p>
@endif
@else
<p>{{ __('Empty') }}</p>
@ -104,23 +104,27 @@
<div class="card">
<a class="btn small oppose" href="{{ route('admin.account.provision', $account->id) }}">
<i class="ph">repeat</i>
<i class="ph ph-repeat"></i>
{{ __('Renew') }}
</a>
<h3 class="large" id="provisioning">{{ __('Provisioning') }}</h3>
@if ($account->provisioning_token)
<div>
<img style="max-width: 15rem;" src="{{ route('provisioning.qrcode', $account->provisioning_token) }}">
<img style="max-width: 15rem;" src="{{ $account->provisioning_qrcode_url }}">
</div>
<form class="inline">
<div>
<input type="text" style="min-width: 40rem;" readonly
value="{{ route('provisioning.provision', $account->provisioning_token) }}">
value="{{ $account->provisioning_url }}">
<small>{{ __('The link can only be visited once') }}</small>
</div>
</form>
<p>
<i class="ph ph-app-window"></i>
<a target="_blank" href="{{ $account->provisioning_wizard_url }}">{{ __('Provisioning wizard URL') }}</a>
</p>
@else
<a class="btn btn-light" href="{{ route('admin.account.provision', $account->id) }}">{{ __('Create') }}</a>
@endif
@ -147,8 +151,8 @@
<tr>
<td class="line">{{ $device->user_agent }}</td>
<td class="actions">
<a type="button" class="btn small tertiary" href="{{ route('account.device.delete', [$device->uuid]) }}">
<i class="ph">trash</i>
<a type="button" class="btn small tertiary" href="{{ route('admin.account.device.delete', [$account->id, $device->uuid]) }}">
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -160,7 +164,7 @@
<div class="card large">
<a class="btn small oppose" href="{{ route('admin.account.dictionary.create', $account) }}">
<i class="ph">plus</i>
<i class="ph ph-plus"></i>
{{ __('Add') }}
</a>
<h3>
@ -188,12 +192,12 @@
<a type="button"
class="btn secondary small"
href="{{ route('admin.account.dictionary.edit', [$account, $dictionaryEntry->key]) }}">
<i class="ph">pencil</i>
<i class="ph ph-pencil"></i>
</a>
<a type="button"
class="btn small tertiary"
href="{{ route('admin.account.dictionary.delete', [$account, $dictionaryEntry->key]) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -206,11 +210,11 @@
<div class="card" id="actions">
@if ($account->dtmf_protocol)
<a class="btn small oppose" href="{{ route('admin.account.action.create', $account) }}">
<i class="ph">plus</i>{{ __('Add') }}
<i class="ph ph-plus"></i>{{ __('Add') }}
</a>
@else
<a class="btn small oppose" href="{{ route('admin.account.edit', $account) }}">
<i class="ph">pencil</i>
<i class="ph ph-pencil"></i>
{{ __('Edit') }}
</a>
@endif
@ -236,11 +240,11 @@
<td class="actions">
<a class="btn small secondary"
href="{{ route('admin.account.action.edit', [$account, $action->id]) }}">
<i class="ph">pencil</i>
<i class="ph ph-pencil"></i>
</a>
<a class="btn small tertiary"
href="{{ route('admin.account.action.delete', [$account, $action->id]) }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@ -254,7 +258,7 @@
<div class="card" id="types">
<a class="btn small oppose" href="{{ route('admin.account.account_type.create', $account) }}">
<i class="ph">plus</i>{{ __('Add') }}
<i class="ph ph-plus"></i>{{ __('Add') }}
</a>
<h3>{{ __('Types') }}</h3>
@ -276,7 +280,7 @@
@csrf
@method('delete')
<button class="btn small tertiary" type="submit" title="{{ __('Delete') }}">
<i class="ph">trash</i>
<i class="ph ph-trash"></i>
</button>
</form>
</td>

View file

@ -9,7 +9,7 @@
@section('content')
<header>
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
<h1><i class="ph ph-users"></i> {{ $account->identifier }}</h1>
</header>
@include('admin.account.parts.tabs')
@ -43,19 +43,19 @@
</form>
</div>
<h2><i class="ph">envelope</i> Messages from the account</h2>
<h2><i class="ph ph-envelope"></i> Messages from the account</h2>
{!! $messagesFromGraph !!}
<h2><i class="ph">envelope</i> Messages to the account</h2>
<h2><i class="ph ph-envelope"></i> Messages to the account</h2>
{!! $messagesToGraph !!}
<h2><i class="ph">phone</i> Calls from the account</h2>
<h2><i class="ph ph-phone"></i> Calls from the account</h2>
{!! $callsFromGraph !!}
<h2><i class="ph">phone</i> Calls to the account</h2>
<h2><i class="ph ph-phone"></i> Calls to the account</h2>
{!! $callsToGraph !!}

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