2026-02-04 23:31:53 +01:00
< ? php
2026-02-11 19:28:12 +01:00
namespace MintyPHP\Repository\User ;
2026-02-04 23:31:53 +01:00
use MintyPHP\DB ;
2026-02-11 19:28:12 +01:00
use MintyPHP\Repository\Support\RepoQuery ;
2026-02-04 23:31:53 +01:00
class UserRepository
{
2026-02-11 19:28:12 +01:00
private static function buildUserFilters ( array $options ) : array
2026-02-04 23:31:53 +01:00
{
$search = trim (( string ) ( $options [ 'search' ] ? ? '' ));
$active = $options [ 'active' ] ? ? null ;
$createdFrom = trim (( string ) ( $options [ 'created_from' ] ? ? '' ));
$createdTo = trim (( string ) ( $options [ 'created_to' ] ? ? '' ));
$tenant = trim (( string ) ( $options [ 'tenant' ] ? ? '' ));
$tenantUuids = array_filter ( array_map ( 'trim' , explode ( ',' , ( string ) ( $options [ 'tenants' ] ? ? '' ))));
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$roleIds = RepoQuery :: normalizeIdList ( $options [ 'roles' ] ? ? []);
$departmentIds = RepoQuery :: normalizeIdList ( $options [ 'departments' ] ? ? []);
2026-02-04 23:31:53 +01:00
$emailVerified = $options [ 'email_verified' ] ? ? null ;
2026-02-11 19:28:12 +01:00
$loginStatus = $options [ 'login_status' ] ? ? null ;
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$customFieldFilterSpec = is_array ( $options [ 'customFieldFilterSpec' ] ? ? null )
? $options [ 'customFieldFilterSpec' ]
: [];
$customFieldFilters = is_array ( $customFieldFilterSpec [ 'filters' ] ? ? null )
? $customFieldFilterSpec [ 'filters' ]
: [];
2026-02-04 23:31:53 +01:00
$where = [];
$params = [];
2026-02-11 19:28:12 +01:00
RepoQuery :: addLikeFilter (
$where ,
$params ,
[ 'users.first_name' , 'users.last_name' , 'users.email' ],
$search
);
RepoQuery :: addEnumFilter ( $where , $params , $active , [
[ 'aliases' => [ '1' , 'true' , 'active' ], 'sql' => 'users.active = ?' , 'params' => [ '1' ]],
[ 'aliases' => [ '0' , 'false' , 'inactive' ], 'sql' => 'users.active = ?' , 'params' => [ '0' ]],
]);
2026-02-04 23:31:53 +01:00
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
if ( $createdFrom !== '' && preg_match ( '/^\d{4}-\d{2}-\d{2}$/' , $createdFrom )) {
2026-02-04 23:31:53 +01:00
$where [] = 'users.created >= ?' ;
$params [] = $createdFrom . ' 00:00:00' ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
if ( $createdTo !== '' && preg_match ( '/^\d{4}-\d{2}-\d{2}$/' , $createdTo )) {
2026-02-04 23:31:53 +01:00
$where [] = 'users.created <= ?' ;
$params [] = $createdTo . ' 23:59:59' ;
}
if ( $tenantUuids ) {
$where [] = 'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid in (???))' ;
$params [] = array_values ( $tenantUuids );
2026-02-11 19:28:12 +01:00
} else {
RepoQuery :: addEqualsFilter (
$where ,
$params ,
$tenant ,
'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid = ?)'
);
2026-02-04 23:31:53 +01:00
}
if ( $roleIds ) {
$where [] = 'exists (select 1 from user_roles ur2 where ur2.user_id = users.id and ur2.role_id in (???))' ;
$params [] = array_map ( 'strval' , $roleIds );
}
if ( $departmentIds ) {
$where [] = 'exists (select 1 from user_departments ud2 where ud2.user_id = users.id and ud2.department_id in (???))' ;
$params [] = array_map ( 'strval' , $departmentIds );
}
2026-02-11 19:28:12 +01:00
RepoQuery :: addEnumFilter ( $where , $params , $emailVerified , [
[ 'aliases' => [ '1' , 'true' , 'verified' , 'yes' ], 'sql' => 'users.email_verified_at is not null' ],
[ 'aliases' => [ '0' , 'false' , 'unverified' , 'no' ], 'sql' => 'users.email_verified_at is null' ],
]);
RepoQuery :: addEnumFilter ( $where , $params , $loginStatus , [
[ 'aliases' => [ 'never' , 'none' , 'no' ], 'sql' => 'users.last_login_at is null' ],
[ 'aliases' => [ 'ever' , 'logged' , 'yes' ], 'sql' => 'users.last_login_at is not null' ],
]);
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
if ( ! empty ( $customFieldFilters [ 'select' ]) && is_array ( $customFieldFilters [ 'select' ])) {
foreach ( $customFieldFilters [ 'select' ] as $definitionId => $optionId ) {
$definitionId = ( int ) $definitionId ;
$optionId = ( int ) $optionId ;
if ( $definitionId <= 0 || $optionId <= 0 ) {
continue ;
}
$where [] = 'exists (select 1 from user_custom_field_values ucfv ' .
'where ucfv.user_id = users.id and ucfv.definition_id = ? and ucfv.option_id = ?)' ;
$params [] = ( string ) $definitionId ;
$params [] = ( string ) $optionId ;
}
}
if ( ! empty ( $customFieldFilters [ 'boolean' ]) && is_array ( $customFieldFilters [ 'boolean' ])) {
foreach ( $customFieldFilters [ 'boolean' ] as $definitionId => $boolValue ) {
$definitionId = ( int ) $definitionId ;
$boolValue = ( int ) $boolValue ;
if ( $definitionId <= 0 || ( $boolValue !== 0 && $boolValue !== 1 )) {
continue ;
}
$where [] = 'exists (select 1 from user_custom_field_values ucfv ' .
'where ucfv.user_id = users.id and ucfv.definition_id = ? and ucfv.value_bool = ?)' ;
$params [] = ( string ) $definitionId ;
$params [] = ( string ) $boolValue ;
}
}
if ( ! empty ( $customFieldFilters [ 'multiselect' ]) && is_array ( $customFieldFilters [ 'multiselect' ])) {
foreach ( $customFieldFilters [ 'multiselect' ] as $definitionId => $optionIds ) {
$definitionId = ( int ) $definitionId ;
$optionIds = RepoQuery :: normalizeIdList ( $optionIds );
if ( $definitionId <= 0 || ! $optionIds ) {
continue ;
}
$where [] = 'exists (select 1 from user_custom_field_values ucfv ' .
'join user_custom_field_value_options ucfvo on ucfvo.value_id = ucfv.id ' .
'where ucfv.user_id = users.id and ucfv.definition_id = ? and ucfvo.option_id in (???))' ;
$params [] = ( string ) $definitionId ;
$params [] = array_map ( 'strval' , $optionIds );
}
}
if ( ! empty ( $customFieldFilters [ 'date' ]) && is_array ( $customFieldFilters [ 'date' ])) {
foreach ( $customFieldFilters [ 'date' ] as $definitionId => $bounds ) {
$definitionId = ( int ) $definitionId ;
$from = is_array ( $bounds ) ? trim (( string ) ( $bounds [ 'from' ] ? ? '' )) : '' ;
$to = is_array ( $bounds ) ? trim (( string ) ( $bounds [ 'to' ] ? ? '' )) : '' ;
if ( $definitionId <= 0 || ( $from === '' && $to === '' )) {
continue ;
}
if ( $from !== '' ) {
$where [] = 'exists (select 1 from user_custom_field_values ucfv ' .
'where ucfv.user_id = users.id and ucfv.definition_id = ? and ucfv.value_date >= ?)' ;
$params [] = ( string ) $definitionId ;
$params [] = $from ;
}
if ( $to !== '' ) {
$where [] = 'exists (select 1 from user_custom_field_values ucfv ' .
'where ucfv.user_id = users.id and ucfv.definition_id = ? and ucfv.value_date <= ?)' ;
$params [] = ( string ) $definitionId ;
$params [] = $to ;
}
}
}
2026-02-04 23:31:53 +01:00
if ( ! empty ( $options [ 'tenantUserId' ])) {
$tenantUserId = ( int ) $options [ 'tenantUserId' ];
if ( $tenantUserId > 0 ) {
if ( defined ( 'TENANT_SCOPE_STRICT' ) && TENANT_SCOPE_STRICT ) {
$where [] = 'exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
" join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id)' ;
$params [] = ( string ) $tenantUserId ;
} else {
$where [] = '(not exists (select 1 from user_tenants ut where ut.user_id = users.id) ' .
'or exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
" join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id))' ;
$params [] = ( string ) $tenantUserId ;
}
}
}
$whereSql = $where ? ( ' where ' . implode ( ' and ' , $where )) : '' ;
2026-02-11 19:28:12 +01:00
return [ $whereSql , $params ];
}
2026-02-04 23:31:53 +01:00
2026-02-11 19:28:12 +01:00
private static function buildListQuery ( string $whereSql , string $order , string $dir ) : string
{
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
return 'select users.id, users.uuid, users.first_name, users.last_name, users.display_name, users.email, users.profile_description, users.job_title, users.phone, users.mobile, users.short_dial, users.address, users.postal_code, users.city, users.country, users.region, users.hire_date, users.theme, users.primary_tenant_id, users.created_by, users.modified_by, users.created, users.modified, users.last_login_at, users.last_login_provider, users.active, ' .
2026-02-04 23:31:53 +01:00
'pt.description as primary_tenant_label ' .
" from users left join tenants pt on pt.id = users.primary_tenant_id and pt.status = 'active' " .
$whereSql .
sprintf ( ' order by `users`.`%s` %s limit ? offset ?' , $order , $dir );
2026-02-11 19:28:12 +01:00
}
2026-02-04 23:31:53 +01:00
2026-02-11 19:28:12 +01:00
private static function extractIds ( array $list ) : array
{
2026-02-04 23:31:53 +01:00
$ids = [];
foreach ( $list as $user ) {
if ( isset ( $user [ 'id' ])) {
$ids [] = ( int ) $user [ 'id' ];
}
}
2026-02-11 19:28:12 +01:00
return $ids ;
}
2026-02-04 23:31:53 +01:00
2026-02-11 19:28:12 +01:00
private static function collectLabels ( array $rows , string $userKey , string $labelKey ) : array
{
$labelsByUser = [];
foreach ( $rows as $row ) {
$userId = 0 ;
$label = '' ;
if ( isset ( $row [ $userKey ]) && is_array ( $row [ $userKey ])) {
$userId = ( int ) ( $row [ $userKey ][ 'user_id' ] ? ? 0 );
}
if ( isset ( $row [ $labelKey ]) && is_array ( $row [ $labelKey ])) {
$label = ( string ) ( $row [ $labelKey ][ 'description' ] ? ? '' );
}
if ( $userId === 0 || $label === '' ) {
foreach ( $row as $value ) {
if ( ! is_array ( $value )) {
continue ;
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
if ( $userId === 0 && isset ( $value [ 'user_id' ])) {
$userId = ( int ) $value [ 'user_id' ];
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
if ( $label === '' && isset ( $value [ 'description' ])) {
$label = ( string ) $value [ 'description' ];
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
}
}
if ( $userId === 0 && isset ( $row [ 'user_id' ])) {
$userId = ( int ) $row [ 'user_id' ];
}
if ( $label === '' && isset ( $row [ 'description' ])) {
$label = ( string ) $row [ 'description' ];
}
if ( $userId > 0 && $label !== '' ) {
$labelsByUser [ $userId ] ? ? = [];
$labelsByUser [ $userId ][] = $label ;
}
}
return $labelsByUser ;
}
private static function hydrateUserLabels ( array $list , array $options ) : array
{
$ids = self :: extractIds ( $list );
if ( ! $ids ) {
return $list ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$scopeUserId = ( int ) ( $options [ 'tenantUserId' ] ? ? 0 );
$scopeToActiveTenants = $scopeUserId > 0 ;
2026-02-11 19:28:12 +01:00
$tenantLabelJoin = $scopeToActiveTenants
? " join tenants t on t.id = ut.tenant_id and t.status = 'active' "
: 'join tenants t on t.id = ut.tenant_id ' ;
$placeholders = implode ( ',' , array_fill ( 0 , count ( $ids ), '?' ));
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$tenantScopeSql = '' ;
$tenantScopeParams = [];
if ( $scopeToActiveTenants ) {
$tenantScopeSql = ' and exists (select 1 from user_tenants uts ' .
" join tenants ts on ts.id = uts.tenant_id and ts.status = 'active' " .
'where uts.user_id = ? and uts.tenant_id = ut.tenant_id)' ;
$tenantScopeParams [] = ( string ) $scopeUserId ;
}
2026-02-11 19:28:12 +01:00
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$labelSql = 'select ut.user_id as user_id, t.id as tenant_id, t.description as description from user_tenants ut ' . $tenantLabelJoin .
'where ut.user_id in (' . $placeholders . ')' . $tenantScopeSql . ' order by t.description asc' ;
$labelRows = call_user_func_array ([ 'MintyPHP\\DB' , 'select' ], array_merge (
[ $labelSql ],
array_merge ( array_map ( 'strval' , $ids ), $tenantScopeParams )
));
$roleRows = DB :: select (
2026-02-11 19:28:12 +01:00
'select ur.user_id as user_id, r.description as description from user_roles ur join roles r on r.id = ur.role_id and r.active = 1 ' .
'where ur.user_id in (' . $placeholders . ') order by r.description asc' ,
... array_map ( 'strval' , $ids )
);
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$departmentScopeSql = '' ;
$departmentScopeParams = [];
if ( $scopeToActiveTenants ) {
$departmentScopeSql = ' and exists (select 1 from user_tenants uts ' .
" join tenants ts on ts.id = uts.tenant_id and ts.status = 'active' " .
'where uts.user_id = ? and uts.tenant_id = d.tenant_id)' ;
$departmentScopeParams [] = ( string ) $scopeUserId ;
}
$departmentLabelSql = 'select ud.user_id as user_id, d.description as description from user_departments ud ' .
'join departments d on d.id = ud.department_id and d.active = 1 ' .
'where ud.user_id in (' . $placeholders . ')' . $departmentScopeSql . ' order by d.description asc' ;
$departmentRows = call_user_func_array ([ 'MintyPHP\\DB' , 'select' ], array_merge (
[ $departmentLabelSql ],
array_merge ( array_map ( 'strval' , $ids ), $departmentScopeParams )
));
2026-02-11 19:28:12 +01:00
$tenantMapByUser = [];
foreach ( $labelRows as $row ) {
$userId = 0 ;
$tenantId = 0 ;
$label = '' ;
if ( isset ( $row [ 'ut' ]) && is_array ( $row [ 'ut' ])) {
$userId = ( int ) ( $row [ 'ut' ][ 'user_id' ] ? ? 0 );
}
if ( isset ( $row [ 't' ]) && is_array ( $row [ 't' ])) {
$tenantId = ( int ) ( $row [ 't' ][ 'tenant_id' ] ? ? 0 );
$label = ( string ) ( $row [ 't' ][ 'description' ] ? ? '' );
}
if ( $userId === 0 || $tenantId === 0 || $label === '' ) {
foreach ( $row as $value ) {
if ( ! is_array ( $value )) {
continue ;
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
if ( $userId === 0 && isset ( $value [ 'user_id' ])) {
$userId = ( int ) $value [ 'user_id' ];
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
if ( $tenantId === 0 && isset ( $value [ 'tenant_id' ])) {
$tenantId = ( int ) $value [ 'tenant_id' ];
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
if ( $label === '' && isset ( $value [ 'description' ])) {
$label = ( string ) $value [ 'description' ];
2026-02-04 23:31:53 +01:00
}
}
}
2026-02-11 19:28:12 +01:00
if ( $userId === 0 && isset ( $row [ 'user_id' ])) {
$userId = ( int ) $row [ 'user_id' ];
}
if ( $tenantId === 0 && isset ( $row [ 'tenant_id' ])) {
$tenantId = ( int ) $row [ 'tenant_id' ];
}
if ( $label === '' && isset ( $row [ 'description' ])) {
$label = ( string ) $row [ 'description' ];
}
if ( $userId > 0 && $tenantId > 0 && $label !== '' ) {
$tenantMapByUser [ $userId ] ? ? = [];
$tenantMapByUser [ $userId ][ $tenantId ] = $label ;
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
}
$tenantLabelsByUser = [];
foreach ( $tenantMapByUser as $userId => $map ) {
$tenantLabelsByUser [ $userId ] = array_values ( $map );
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$roleLabelsByUser = self :: collectLabels ( $roleRows , 'ur' , 'r' );
$departmentLabelsByUser = self :: collectLabels ( $departmentRows , 'ud' , 'd' );
2026-02-11 19:28:12 +01:00
foreach ( $list as & $user ) {
$userId = ( int ) ( $user [ 'id' ] ? ? 0 );
$primaryId = ( int ) ( $user [ 'primary_tenant_id' ] ? ? 0 );
$user [ 'tenant_labels' ] = $tenantLabelsByUser [ $userId ] ? ? [];
if ( $primaryId > 0 && isset ( $tenantMapByUser [ $userId ][ $primaryId ])) {
$user [ 'primary_tenant_label' ] = $tenantMapByUser [ $userId ][ $primaryId ];
}
$user [ 'role_labels' ] = $roleLabelsByUser [ $userId ] ? ? [];
$user [ 'department_labels' ] = $departmentLabelsByUser [ $userId ] ? ? [];
}
unset ( $user );
2026-02-04 23:31:53 +01:00
2026-02-11 19:28:12 +01:00
return $list ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
2026-02-11 19:28:12 +01:00
private static function unwrap ( ? array $row ) : ? array
{
if ( ! $row ) {
return null ;
}
return $row [ 'users' ] ? ? null ;
}
private static function unwrapList ( $rows ) : array
{
if ( ! is_array ( $rows )) {
return [];
}
$list = [];
foreach ( $rows as $row ) {
$user = $row [ 'users' ] ? ? null ;
if ( is_array ( $user )) {
foreach ( $row as $key => $value ) {
if ( $key === 'users' || is_array ( $value )) {
continue ;
}
$user [ $key ] = $value ;
}
if ( isset ( $row [ 'pt' ]) && is_array ( $row [ 'pt' ])) {
$label = ( string ) ( $row [ 'pt' ][ 'description' ] ? ? '' );
if ( $label !== '' ) {
$user [ 'primary_tenant_label' ] = $label ;
}
2026-02-04 23:31:53 +01:00
}
2026-02-11 19:28:12 +01:00
$list [] = $user ;
2026-02-04 23:31:53 +01:00
}
}
2026-02-11 19:28:12 +01:00
return $list ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
public static function updateLastLogin ( int $userId , string $provider = 'local' ) : void
2026-02-11 19:28:12 +01:00
{
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
if ( $userId <= 0 ) {
return ;
}
$provider = trim ( strtolower ( $provider ));
if ( ! in_array ( $provider , [ 'local' , 'microsoft' ], true )) {
$provider = 'local' ;
}
DB :: query ( 'update users set last_login_at = UTC_TIMESTAMP(), last_login_provider = ? where id = ?' , $provider , ( string ) $userId );
}
public static function findAuthzSnapshot ( int $userId ) : ? array
{
if ( $userId <= 0 ) {
return null ;
}
$row = DB :: selectOne (
'select id, active, authz_version, locale, theme, current_tenant_id from users where id = ? limit 1' ,
( string ) $userId
2026-02-11 19:28:12 +01:00
);
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
return self :: unwrap ( $row );
2026-02-11 19:28:12 +01:00
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
public static function bumpAuthzVersion ( int $userId ) : bool
2026-02-11 19:28:12 +01:00
{
if ( $userId <= 0 ) {
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
return false ;
2026-02-11 19:28:12 +01:00
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$result = DB :: update (
'update users set authz_version = authz_version + 1 where id = ?' ,
( string ) $userId
);
return $result !== false ;
}
public static function bumpAuthzVersionByUserIds ( array $userIds ) : int
{
$userIds = array_values ( array_unique ( array_map ( 'intval' , $userIds )));
$userIds = array_values ( array_filter ( $userIds , static fn ( $id ) => $id > 0 ));
if ( ! $userIds ) {
return 0 ;
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $userIds ), '?' ));
$result = DB :: update (
" update users set authz_version = authz_version + 1 where id in ( $placeholders ) " ,
... array_map ( 'strval' , $userIds )
);
return $result !== false ? ( int ) $result : 0 ;
2026-02-11 19:28:12 +01:00
}
public static function listPaged ( array $options ) : array
{
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$allowedOrder = [ 'id' , 'uuid' , 'first_name' , 'last_name' , 'display_name' , 'email' , 'created' , 'modified' , 'active' , 'last_login_at' ];
2026-02-11 19:28:12 +01:00
[ $limit , $offset ] = RepoQuery :: sanitizeLimitOffset ( $options );
[ $order , $dir ] = RepoQuery :: sanitizeOrder ( $options , $allowedOrder );
[ $whereSql , $params ] = self :: buildUserFilters ( $options );
$count = DB :: selectValue ( 'select count(*) from users' . $whereSql , ... $params );
$total = $count ? ( int ) $count : 0 ;
$query = self :: buildListQuery ( $whereSql , $order , $dir );
$queryParams = array_merge ( $params , [( string ) $limit , ( string ) $offset ]);
$rows = call_user_func_array ([ 'MintyPHP\\DB' , 'select' ], array_merge ([ $query ], $queryParams ));
$list = self :: unwrapList ( $rows );
$list = self :: hydrateUserLabels ( $list , $options );
2026-02-04 23:31:53 +01:00
$result = [
'total' => $total ,
'rows' => $list ,
];
return $result ;
}
public static function find ( int $id ) : ? array
{
$row = DB :: selectOne (
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, last_login_at, last_login_provider, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, modified_by, created, modified, active, authz_version from users where id = ? limit 1' ,
2026-02-04 23:31:53 +01:00
( string ) $id
);
return self :: unwrap ( $row );
}
public static function findByUuid ( string $uuid ) : ? array
{
$row = DB :: selectOne (
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, last_login_at, last_login_provider, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, modified_by, created, modified, active, authz_version, active_changed_at, active_changed_by from users where uuid = ? limit 1' ,
2026-02-04 23:31:53 +01:00
$uuid
);
return self :: unwrap ( $row );
}
public static function findByEmail ( string $email ) : ? array
{
$row = DB :: selectOne (
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, last_login_at, last_login_provider, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, modified_by, created, modified, active, authz_version, active_changed_at, active_changed_by from users where email = ? limit 1' ,
2026-02-04 23:31:53 +01:00
$email
);
return self :: unwrap ( $row );
}
2026-02-11 19:28:12 +01:00
private static function buildDisplayName ( array $data ) : string
{
$first = trim (( string ) ( $data [ 'first_name' ] ? ? '' ));
$last = trim (( string ) ( $data [ 'last_name' ] ? ? '' ));
$name = trim ( $first . ' ' . $last );
if ( $name === '' ) {
$name = trim (( string ) ( $data [ 'email' ] ? ? '' ));
}
return $name ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
public static function create ( array $data ) : int | false
2026-02-04 23:31:53 +01:00
{
$hash = password_hash ( $data [ 'password' ], PASSWORD_DEFAULT );
return DB :: insert (
2026-02-11 19:28:12 +01:00
'insert into users (uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, created, active, active_changed_at, active_changed_by) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,?,?)' ,
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
$data [ 'uuid' ] ? ? RepoQuery :: uuidV4 (),
2026-02-04 23:31:53 +01:00
$data [ 'first_name' ],
$data [ 'last_name' ],
2026-02-11 19:28:12 +01:00
self :: buildDisplayName ( $data ),
2026-02-04 23:31:53 +01:00
$data [ 'email' ],
$data [ 'profile_description' ] ? ? null ,
$data [ 'job_title' ] ? ? null ,
$data [ 'phone' ] ? ? null ,
$data [ 'mobile' ] ? ? null ,
$data [ 'short_dial' ] ? ? null ,
$data [ 'address' ] ? ? null ,
$data [ 'postal_code' ] ? ? null ,
$data [ 'city' ] ? ? null ,
$data [ 'country' ] ? ? null ,
$data [ 'region' ] ? ? null ,
$data [ 'hire_date' ] ? ? null ,
$hash ,
$data [ 'locale' ] ? ? null ,
$data [ 'totp_secret' ],
$data [ 'theme' ] ? ? 'light' ,
$data [ 'primary_tenant_id' ] ? ? null ,
$data [ 'current_tenant_id' ] ? ? $data [ 'primary_tenant_id' ] ? ? null ,
$data [ 'created_by' ] ? ? null ,
$data [ 'active' ],
$data [ 'active_changed_at' ] ? ? null ,
$data [ 'active_changed_by' ] ? ? null
);
}
public static function update ( int $id , array $data ) : bool
{
$fields = [
'first_name' => $data [ 'first_name' ],
'last_name' => $data [ 'last_name' ],
2026-02-11 19:28:12 +01:00
'display_name' => self :: buildDisplayName ( $data ),
2026-02-04 23:31:53 +01:00
'email' => $data [ 'email' ],
'profile_description' => $data [ 'profile_description' ] ? ? null ,
'job_title' => $data [ 'job_title' ] ? ? null ,
'phone' => $data [ 'phone' ] ? ? null ,
'mobile' => $data [ 'mobile' ] ? ? null ,
'short_dial' => $data [ 'short_dial' ] ? ? null ,
'address' => $data [ 'address' ] ? ? null ,
'postal_code' => $data [ 'postal_code' ] ? ? null ,
'city' => $data [ 'city' ] ? ? null ,
'country' => $data [ 'country' ] ? ? null ,
'region' => $data [ 'region' ] ? ? null ,
'hire_date' => $data [ 'hire_date' ] ? ? null ,
'totp_secret' => $data [ 'totp_secret' ],
'active' => $data [ 'active' ],
];
if ( array_key_exists ( 'locale' , $data )) {
$fields [ 'locale' ] = $data [ 'locale' ];
}
if ( array_key_exists ( 'theme' , $data )) {
$fields [ 'theme' ] = $data [ 'theme' ];
}
if ( array_key_exists ( 'primary_tenant_id' , $data )) {
$fields [ 'primary_tenant_id' ] = $data [ 'primary_tenant_id' ];
}
if ( array_key_exists ( 'current_tenant_id' , $data )) {
$fields [ 'current_tenant_id' ] = $data [ 'current_tenant_id' ];
}
if ( array_key_exists ( 'modified_by' , $data )) {
$fields [ 'modified_by' ] = $data [ 'modified_by' ];
}
if ( array_key_exists ( 'active_changed_at' , $data )) {
$fields [ 'active_changed_at' ] = $data [ 'active_changed_at' ];
}
if ( array_key_exists ( 'active_changed_by' , $data )) {
$fields [ 'active_changed_by' ] = $data [ 'active_changed_by' ];
}
if ( ! empty ( $data [ 'password' ])) {
$fields [ 'password' ] = password_hash ( $data [ 'password' ], PASSWORD_DEFAULT );
}
$setParts = [];
$params = [];
foreach ( $fields as $field => $value ) {
$setParts [] = sprintf ( '`%s` = ?' , $field );
$params [] = $value ;
}
$params [] = ( string ) $id ;
$query = 'update users set ' . implode ( ', ' , $setParts ) . ' where id = ?' ;
$result = DB :: update ( $query , ... $params );
return $result !== false ;
}
public static function setActive ( int $id , bool $active , ? int $changedBy = null ) : bool
{
$result = DB :: update (
'update users set active = ?, active_changed_at = NOW(), active_changed_by = ? where id = ?' ,
$active ? 1 : 0 ,
$changedBy ,
( string ) $id
);
return $result !== false ;
}
public static function setCurrentTenant ( int $id , int $tenantId ) : bool
{
$result = DB :: update (
'update users set current_tenant_id = ? where id = ?' ,
( string ) $tenantId ,
( string ) $id
);
return $result !== false ;
}
public static function setActiveByUuids ( array $uuids , bool $active , ? int $modifiedBy = null ) : bool
{
$uuids = array_values ( array_filter ( array_map ( 'trim' , $uuids )));
if ( ! $uuids ) {
return false ;
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $uuids ), '?' ));
$query = " update users set active = ?, modified_by = ?, active_changed_at = NOW(), active_changed_by = ? where uuid in ( $placeholders ) " ;
$params = array_merge ([ $active ? 1 : 0 , $modifiedBy , $modifiedBy ], $uuids );
$result = DB :: update ( $query , ... $params );
return $result !== false ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
public static function listPrivilegedUserIdsByPermissionKeys ( array $permissionKeys ) : array
{
$keys = array_values ( array_unique ( array_filter ( array_map (
static fn ( $key ) => trim (( string ) $key ),
$permissionKeys
))));
if ( ! $keys ) {
return [];
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $keys ), '?' ));
$rows = DB :: select (
'select distinct ur.user_id from user_roles ur ' .
'join roles r on r.id = ur.role_id and r.active = 1 ' .
'join role_permissions rp on rp.role_id = ur.role_id ' .
'join permissions p on p.id = rp.permission_id and p.active = 1 ' .
" where p.`key` in ( $placeholders ) " ,
... $keys
);
if ( ! is_array ( $rows )) {
return [];
}
$ids = [];
foreach ( $rows as $row ) {
$data = $row [ 'ur' ] ? ? $row [ 'user_roles' ] ? ? $row ;
$userId = ( int ) ( $data [ 'user_id' ] ? ? $row [ 'user_id' ] ? ? 0 );
if ( $userId > 0 ) {
$ids [] = $userId ;
}
}
return array_values ( array_unique ( $ids ));
}
public static function listIdsForAutoDeactivate ( int $days , array $excludedUserIds , int $limit = 500 ) : array
{
if ( $days <= 0 || $limit <= 0 ) {
return [];
}
$excluded = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $excludedUserIds ), static fn ( $id ) => $id > 0 )));
$query = 'select id from users where active = 1 and coalesce(last_login_at, created) <= DATE_SUB(UTC_TIMESTAMP(), INTERVAL ' .
( int ) $days .
' DAY)' ;
$params = [];
if ( $excluded ) {
$placeholders = implode ( ',' , array_fill ( 0 , count ( $excluded ), '?' ));
$query .= " and id not in ( $placeholders ) " ;
$params = array_map ( 'strval' , $excluded );
}
$query .= ' order by coalesce(last_login_at, created) asc limit ?' ;
$params [] = ( string ) $limit ;
$rows = DB :: select ( $query , ... $params );
if ( ! is_array ( $rows )) {
return [];
}
return self :: extractIdList ( $rows );
}
public static function listIdsForAutoDelete ( int $days , array $excludedUserIds , int $limit = 500 ) : array
{
if ( $days <= 0 || $limit <= 0 ) {
return [];
}
$excluded = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $excludedUserIds ), static fn ( $id ) => $id > 0 )));
$query = 'select id from users where active = 0 and active_changed_at is not null and active_changed_at <= DATE_SUB(UTC_TIMESTAMP(), INTERVAL ' .
( int ) $days .
' DAY)' ;
$params = [];
if ( $excluded ) {
$placeholders = implode ( ',' , array_fill ( 0 , count ( $excluded ), '?' ));
$query .= " and id not in ( $placeholders ) " ;
$params = array_map ( 'strval' , $excluded );
}
$query .= ' order by active_changed_at asc limit ?' ;
$params [] = ( string ) $limit ;
$rows = DB :: select ( $query , ... $params );
if ( ! is_array ( $rows )) {
return [];
}
return self :: extractIdList ( $rows );
}
public static function setInactiveByIds ( array $userIds , ? int $changedBy = null ) : int
{
$ids = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $userIds ), static fn ( $id ) => $id > 0 )));
if ( ! $ids ) {
return 0 ;
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $ids ), '?' ));
$query = " update users set active = 0, modified_by = ?, active_changed_at = UTC_TIMESTAMP(), active_changed_by = ? where active = 1 and id in ( $placeholders ) " ;
$params = array_merge ([ $changedBy , $changedBy ], array_map ( 'strval' , $ids ));
$result = DB :: update ( $query , ... $params );
return $result !== false ? ( int ) $result : 0 ;
}
public static function deleteByIds ( array $userIds ) : int
{
$ids = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $userIds ), static fn ( $id ) => $id > 0 )));
if ( ! $ids ) {
return 0 ;
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $ids ), '?' ));
$result = DB :: delete (
" delete from users where id in ( $placeholders ) " ,
... array_map ( 'strval' , $ids )
);
return $result !== false ? ( int ) $result : 0 ;
}
2026-02-04 23:31:53 +01:00
public static function setLocale ( int $id , string $locale ) : bool
{
$result = DB :: update ( 'update users set locale = ? where id = ?' , $locale , ( string ) $id );
return $result !== false ;
}
public static function setTheme ( int $id , string $theme ) : bool
{
$result = DB :: update ( 'update users set theme = ? where id = ?' , $theme , ( string ) $id );
return $result !== false ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
public static function updateProfileFieldsFromSso ( int $id , array $fields ) : bool
{
if ( $id <= 0 ) {
return false ;
}
$existing = self :: find ( $id );
if ( ! $existing ) {
return false ;
}
$allowed = [ 'first_name' , 'last_name' , 'phone' , 'mobile' ];
$updates = [];
foreach ( $allowed as $field ) {
if ( ! array_key_exists ( $field , $fields )) {
continue ;
}
$value = trim (( string ) $fields [ $field ]);
if ( $value === '' ) {
continue ;
}
if (( string ) ( $existing [ $field ] ? ? '' ) === $value ) {
continue ;
}
$updates [ $field ] = $value ;
}
if ( ! $updates ) {
return true ;
}
if ( isset ( $updates [ 'first_name' ]) || isset ( $updates [ 'last_name' ])) {
$displayData = [
'first_name' => $updates [ 'first_name' ] ? ? ( string ) ( $existing [ 'first_name' ] ? ? '' ),
'last_name' => $updates [ 'last_name' ] ? ? ( string ) ( $existing [ 'last_name' ] ? ? '' ),
'email' => ( string ) ( $existing [ 'email' ] ? ? '' ),
];
$updates [ 'display_name' ] = self :: buildDisplayName ( $displayData );
}
$setParts = [];
$params = [];
foreach ( $updates as $field => $value ) {
$setParts [] = sprintf ( '`%s` = ?' , $field );
$params [] = $value ;
}
$params [] = ( string ) $id ;
$result = DB :: update (
'update users set ' . implode ( ', ' , $setParts ) . ' where id = ?' ,
... $params
);
return $result !== false ;
}
2026-02-04 23:31:53 +01:00
public static function setPassword ( int $id , string $password ) : bool
{
$hash = password_hash ( $password , PASSWORD_DEFAULT );
$result = DB :: update ( 'update users set password = ?, modified_by = NULL where id = ?' , $hash , ( string ) $id );
return $result !== false ;
}
public static function setEmailVerified ( int $id ) : bool
{
$result = DB :: update ( 'update users set email_verified_at = NOW() where id = ?' , ( string ) $id );
return $result !== false ;
}
public static function delete ( int $id ) : bool
{
$result = DB :: delete ( 'delete from users where id = ?' , ( string ) $id );
return $result !== false ;
}
public static function deleteByUuids ( array $uuids ) : bool
{
$uuids = array_values ( array_filter ( array_map ( 'trim' , $uuids )));
if ( ! $uuids ) {
return false ;
}
$placeholders = implode ( ',' , array_fill ( 0 , count ( $uuids ), '?' ));
$query = " delete from users where uuid in ( $placeholders ) " ;
$result = DB :: delete ( $query , ... $uuids );
return $result !== false ;
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
private static function extractIdList ( array $rows ) : array
2026-02-04 23:31:53 +01:00
{
$ids = [];
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
foreach ( $rows as $row ) {
$data = $row [ 'users' ] ? ? $row ;
$id = ( int ) ( $data [ 'id' ] ? ? $row [ 'id' ] ? ? 0 );
if ( $id > 0 ) {
$ids [] = $id ;
2026-02-04 23:31:53 +01:00
}
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
return array_values ( array_unique ( $ids ));
2026-02-04 23:31:53 +01:00
}
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
2026-02-04 23:31:53 +01:00
}