2019-05-09 06:54:39 +00:00
< ? php
2024-03-01 02:07:58 +00:00
use Appwrite\Auth\MFA\Type\TOTP ;
2023-01-09 05:46:02 +00:00
use Appwrite\Auth\Validator\Phone ;
2021-05-06 22:31:05 +00:00
use Appwrite\Detector\Detector ;
2022-05-08 15:25:01 +00:00
use Appwrite\Event\Delete ;
2022-05-03 11:57:26 +00:00
use Appwrite\Event\Event ;
2022-05-08 15:25:01 +00:00
use Appwrite\Event\Mail ;
2023-09-05 17:10:48 +00:00
use Appwrite\Event\Messaging ;
2025-01-30 04:53:53 +00:00
use Appwrite\Event\StatsUsage ;
2022-05-03 11:57:26 +00:00
use Appwrite\Extend\Exception ;
2025-11-11 13:25:10 +00:00
use Appwrite\Network\Validator\Email as EmailValidator ;
2025-04-14 11:56:42 +00:00
use Appwrite\Network\Validator\Redirect ;
2024-05-29 19:52:22 +00:00
use Appwrite\Platform\Workers\Deletes ;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\AuthType ;
use Appwrite\SDK\ContentType ;
use Appwrite\SDK\Method ;
use Appwrite\SDK\Response as SDKResponse ;
2022-01-18 11:05:04 +00:00
use Appwrite\Template\Template ;
2025-11-04 06:08:35 +00:00
use Appwrite\Utopia\Database\Documents\User ;
2022-01-18 11:05:04 +00:00
use Appwrite\Utopia\Database\Validator\CustomId ;
2022-08-24 18:25:15 +00:00
use Appwrite\Utopia\Database\Validator\Queries\Memberships ;
2022-08-23 08:56:28 +00:00
use Appwrite\Utopia\Database\Validator\Queries\Teams ;
2022-05-03 11:57:26 +00:00
use Appwrite\Utopia\Request ;
2022-01-18 11:05:04 +00:00
use Appwrite\Utopia\Response ;
2026-01-15 13:19:34 +00:00
use libphonenumber\NumberParseException ;
2025-01-08 11:31:01 +00:00
use libphonenumber\PhoneNumberUtil ;
2022-05-03 11:57:26 +00:00
use MaxMind\Db\Reader ;
2022-04-21 14:07:08 +00:00
use Utopia\Audit\Audit ;
2025-11-04 06:08:35 +00:00
use Utopia\Auth\Proofs\Password ;
use Utopia\Auth\Proofs\Token ;
use Utopia\Auth\Store ;
2020-03-28 12:42:16 +00:00
use Utopia\Config\Config ;
2021-10-05 10:30:33 +00:00
use Utopia\Database\Database ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\DateTime ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Document ;
2021-12-07 08:01:09 +00:00
use Utopia\Database\Exception\Authorization as AuthorizationException ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Exception\Duplicate ;
2025-04-16 11:39:53 +00:00
use Utopia\Database\Exception\Order as OrderException ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Exception\Query as QueryException ;
2025-09-04 11:47:59 +00:00
use Utopia\Database\Exception\Structure as StructureException ;
2022-12-14 15:42:25 +00:00
use Utopia\Database\Helpers\ID ;
2022-12-14 16:04:06 +00:00
use Utopia\Database\Helpers\Permission ;
use Utopia\Database\Helpers\Role ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Query ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Validator\Authorization ;
use Utopia\Database\Validator\Key ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Queries ;
2024-10-17 05:41:24 +00:00
use Utopia\Database\Validator\Query\Cursor ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Query\Limit ;
use Utopia\Database\Validator\Query\Offset ;
2021-07-25 14:47:18 +00:00
use Utopia\Database\Validator\UID ;
2025-11-11 13:25:10 +00:00
use Utopia\Emails\Email ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Http ;
2024-03-08 12:57:20 +00:00
use Utopia\Locale\Locale ;
2024-04-02 06:13:28 +00:00
use Utopia\System\System ;
2022-01-18 11:05:04 +00:00
use Utopia\Validator\ArrayList ;
2023-03-06 14:24:02 +00:00
use Utopia\Validator\Assoc ;
2025-11-05 02:28:56 +00:00
use Utopia\Validator\Boolean ;
2025-10-19 13:15:13 +00:00
use Utopia\Validator\Text ;
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/teams' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Create team' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].create' )
2019-05-09 06:54:39 +00:00
-> label ( 'scope' , 'teams.write' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'team.create' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'team/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'create' ,
description : '/docs/references/teams/create-team.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TEAM ,
)
]
))
2023-01-20 22:22:16 +00:00
-> param ( 'teamId' , '' , new CustomId (), 'Team ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
2020-09-10 14:40:14 +00:00
-> param ( 'name' , null , new Text ( 128 ), 'Team name. Max length: 128 chars.' )
2023-10-13 13:43:23 +00:00
-> param ( 'roles' , [ 'owner' ], new ArrayList ( new Key (), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.' , true )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $teamId , string $name , array $roles , Response $response , Document $user , Database $dbForProject , Authorization $authorization , Event $queueForEvents ) {
2020-06-30 11:09:28 +00:00
2026-01-14 15:08:00 +00:00
$isPrivilegedUser = User :: isPrivileged ( $authorization -> getRoles ());
$isAppUser = User :: isApp ( $authorization -> getRoles ());
2020-11-20 06:48:15 +00:00
2022-08-14 14:22:38 +00:00
$teamId = $teamId == 'unique()' ? ID :: unique () : $teamId ;
2023-07-12 19:03:38 +00:00
2023-07-09 11:20:09 +00:00
try {
2026-01-14 15:08:00 +00:00
$team = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'teams' , new Document ([
2023-07-09 11:20:09 +00:00
'$id' => $teamId ,
'$permissions' => [
Permission :: read ( Role :: team ( $teamId )),
Permission :: update ( Role :: team ( $teamId , 'owner' )),
Permission :: delete ( Role :: team ( $teamId , 'owner' )),
],
2026-01-16 12:23:46 +00:00
'labels' => [],
2023-07-09 11:20:09 +00:00
'name' => $name ,
'total' => ( $isPrivilegedUser || $isAppUser ) ? 0 : 1 ,
'prefs' => new \stdClass (),
'search' => implode ( ' ' , [ $teamId , $name ]),
])));
} catch ( Duplicate $th ) {
2023-07-12 19:03:38 +00:00
throw new Exception ( Exception :: TEAM_ALREADY_EXISTS );
2023-07-09 11:20:09 +00:00
}
2020-06-30 11:09:28 +00:00
2021-03-01 21:04:53 +00:00
if ( ! $isPrivilegedUser && ! $isAppUser ) { // Don't add user on server mode
2022-08-27 23:01:46 +00:00
if ( ! \in_array ( 'owner' , $roles )) {
$roles [] = 'owner' ;
}
2022-08-14 14:22:38 +00:00
$membershipId = ID :: unique ();
2020-06-30 11:09:28 +00:00
$membership = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => $membershipId ,
2022-08-02 09:21:53 +00:00
'$permissions' => [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: read ( Role :: team ( $team -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: team ( $team -> getId (), 'owner' )),
Permission :: delete ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: team ( $team -> getId (), 'owner' )),
2022-08-02 09:21:53 +00:00
],
2022-08-15 11:24:31 +00:00
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2022-08-15 11:24:31 +00:00
'teamId' => $team -> getId (),
2025-05-26 05:42:11 +00:00
'teamInternalId' => $team -> getSequence (),
2020-06-30 11:09:28 +00:00
'roles' => $roles ,
2022-07-13 14:02:49 +00:00
'invited' => DateTime :: now (),
'joined' => DateTime :: now (),
2020-06-30 11:09:28 +00:00
'confirm' => true ,
'secret' => '' ,
2022-02-16 16:26:19 +00:00
'search' => implode ( ' ' , [ $membershipId , $user -> getId ()])
2019-05-09 06:54:39 +00:00
]);
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> createDocument ( 'memberships' , $membership );
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2019-05-09 06:54:39 +00:00
}
2020-06-30 11:09:28 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'teamId' , $team -> getId ());
2022-04-13 12:39:31 +00:00
2021-06-16 17:43:06 +00:00
if ( ! empty ( $user -> getId ())) {
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2021-06-16 17:43:06 +00:00
}
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $team , Response :: MODEL_TEAM );
2020-12-26 16:48:43 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams' )
2023-08-01 15:26:48 +00:00
-> desc ( 'List teams' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2020-01-31 22:34:07 +00:00
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'list' ,
description : '/docs/references/teams/list-teams.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TEAM_LIST ,
)
]
))
2023-03-29 19:38:39 +00:00
-> param ( 'queries' , [], new Teams (), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode ( ', ' , Teams :: ALLOWED_ATTRIBUTES ), true )
2020-09-10 14:40:14 +00:00
-> param ( 'search' , '' , new Text ( 256 ), 'Search term to filter your list results. Max length: 256 chars.' , true )
2025-11-05 02:28:56 +00:00
-> param ( 'total' , true , new Boolean ( true ), 'When set to false, the total count returned will be 0 and will not be calculated.' , true )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2025-11-05 02:28:56 +00:00
-> action ( function ( array $queries , string $search , bool $includeTotal , Response $response , Database $dbForProject ) {
2021-05-06 22:31:05 +00:00
2024-02-12 16:02:04 +00:00
try {
$queries = Query :: parseQueries ( $queries );
} catch ( QueryException $e ) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $e -> getMessage ());
}
2021-08-06 12:36:35 +00:00
2022-08-11 23:53:52 +00:00
if ( ! empty ( $search )) {
2022-08-23 08:56:28 +00:00
$queries [] = Query :: search ( 'search' , $search );
2021-08-06 12:36:35 +00:00
}
2026-01-28 12:53:24 +00:00
$cursor = Query :: getCursorQueries ( $queries , false );
$cursor = \reset ( $cursor );
2024-10-17 05:41:24 +00:00
2026-01-28 12:53:24 +00:00
if ( $cursor !== false ) {
2024-10-17 05:41:24 +00:00
$validator = new Cursor ();
if ( ! $validator -> isValid ( $cursor )) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $validator -> getDescription ());
}
2022-08-23 08:56:28 +00:00
$teamId = $cursor -> getValue ();
$cursorDocument = $dbForProject -> getDocument ( 'teams' , $teamId );
2021-08-18 13:42:03 +00:00
2022-08-11 23:53:52 +00:00
if ( $cursorDocument -> isEmpty ()) {
2022-08-23 08:56:28 +00:00
throw new Exception ( Exception :: GENERAL_CURSOR_NOT_FOUND , " Team ' { $teamId } ' for the 'cursor' value not found. " );
2022-08-11 23:53:52 +00:00
}
2022-08-23 08:56:28 +00:00
$cursor -> setValue ( $cursorDocument );
2021-08-18 13:42:03 +00:00
}
2021-10-06 14:11:04 +00:00
2022-08-23 08:56:28 +00:00
$filterQueries = Query :: groupByType ( $queries )[ 'filters' ];
2025-04-16 11:39:53 +00:00
try {
$results = $dbForProject -> find ( 'teams' , $queries );
2025-11-05 02:28:56 +00:00
$total = $includeTotal ? $dbForProject -> count ( 'teams' , $filterQueries , APP_LIMIT_COUNT ) : 0 ;
2025-04-16 11:39:53 +00:00
} catch ( OrderException $e ) {
2025-04-17 04:46:26 +00:00
throw new Exception ( Exception :: DATABASE_QUERY_ORDER_NULL , " The order attribute ' { $e -> getAttribute () } ' had a null value. Cursor pagination requires all documents order attribute values are non-null. " );
2025-04-16 11:39:53 +00:00
}
2020-06-30 11:09:28 +00:00
2021-07-25 14:47:18 +00:00
$response -> dynamic ( new Document ([
2021-05-27 10:09:14 +00:00
'teams' => $results ,
2022-02-27 09:57:09 +00:00
'total' => $total ,
2020-08-06 14:49:29 +00:00
]), Response :: MODEL_TEAM_LIST );
2020-12-26 16:48:43 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams/:teamId' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Get team' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2020-01-31 22:34:07 +00:00
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'get' ,
description : '/docs/references/teams/get-team.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TEAM ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-05-03 13:21:25 +00:00
-> action ( function ( string $teamId , Response $response , Database $dbForProject ) {
2020-01-31 22:34:07 +00:00
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2020-01-31 22:34:07 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-01-31 22:34:07 +00:00
}
2020-06-30 11:09:28 +00:00
2021-07-25 14:47:18 +00:00
$response -> dynamic ( $team , Response :: MODEL_TEAM );
2020-12-26 16:48:43 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams/:teamId/prefs' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Get team preferences' )
2023-03-06 14:24:02 +00:00
-> groups ([ 'api' , 'teams' ])
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'getPrefs' ,
description : '/docs/references/teams/get-team-prefs.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_PREFERENCES ,
)
]
))
2023-03-06 14:24:02 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> action ( function ( string $teamId , Response $response , Database $dbForProject ) {
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
if ( $team -> isEmpty ()) {
throw new Exception ( Exception :: TEAM_NOT_FOUND );
}
2023-04-12 16:03:08 +00:00
$prefs = $team -> getAttribute ( 'prefs' , []);
2023-03-06 14:24:02 +00:00
2025-09-04 11:59:59 +00:00
try {
$prefs = new Document ( $prefs );
} catch ( StructureException $e ) {
throw new Exception ( Exception :: DOCUMENT_INVALID_STRUCTURE , $e -> getMessage ());
}
$response -> dynamic ( $prefs , Response :: MODEL_PREFERENCES );
2023-03-06 14:24:02 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/teams/:teamId' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update name' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].update' )
2019-05-09 06:54:39 +00:00
-> label ( 'scope' , 'teams.write' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'team.update' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'team/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'updateName' ,
description : '/docs/references/teams/update-team-name.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TEAM ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2021-12-10 15:44:54 +00:00
-> param ( 'name' , null , new Text ( 128 ), 'New team name. Max length: 128 chars.' )
2023-01-20 00:36:17 +00:00
-> inject ( 'requestTimestamp' )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2023-10-15 17:41:09 +00:00
-> action ( function ( string $teamId , string $name , ? \DateTime $requestTimestamp , Response $response , Database $dbForProject , Event $queueForEvents ) {
2019-05-09 06:54:39 +00:00
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2019-05-09 06:54:39 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2023-01-20 00:36:17 +00:00
$team
2021-08-14 18:56:28 +00:00
-> setAttribute ( 'name' , $name )
2023-01-20 00:36:17 +00:00
-> setAttribute ( 'search' , implode ( ' ' , [ $teamId , $name ]));
2025-04-30 05:40:47 +00:00
$team = $dbForProject -> updateDocument ( 'teams' , $team -> getId (), $team );
2019-05-09 06:54:39 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'teamId' , $team -> getId ());
2022-04-13 12:39:31 +00:00
2021-07-25 14:47:18 +00:00
$response -> dynamic ( $team , Response :: MODEL_TEAM );
2020-12-26 16:48:43 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/teams/:teamId/prefs' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update preferences' )
2023-03-06 14:24:02 +00:00
-> groups ([ 'api' , 'teams' ])
-> label ( 'event' , 'teams.[teamId].update.prefs' )
-> label ( 'scope' , 'teams.write' )
-> label ( 'audits.event' , 'team.update' )
-> label ( 'audits.resource' , 'team/{response.$id}' )
-> label ( 'audits.userId' , '{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'updatePrefs' ,
description : '/docs/references/teams/update-team-prefs.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_PREFERENCES ,
)
]
))
2023-03-06 14:24:02 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
-> param ( 'prefs' , '' , new Assoc (), 'Prefs key-value JSON object.' )
-> inject ( 'response' )
-> inject ( 'dbForProject' )
2023-09-27 15:51:17 +00:00
-> inject ( 'queueForEvents' )
-> action ( function ( string $teamId , array $prefs , Response $response , Database $dbForProject , Event $queueForEvents ) {
2025-09-04 11:47:59 +00:00
try {
2025-09-04 12:06:38 +00:00
$prefs = new Document ( $prefs );
2025-09-04 11:47:59 +00:00
} catch ( StructureException $e ) {
throw new Exception ( Exception :: DOCUMENT_INVALID_STRUCTURE , $e -> getMessage ());
}
2023-03-06 14:24:02 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
if ( $team -> isEmpty ()) {
throw new Exception ( Exception :: TEAM_NOT_FOUND );
}
2025-09-04 12:28:59 +00:00
$team = $dbForProject -> updateDocument ( 'teams' , $team -> getId (), new Document ([
'prefs' => $prefs -> getArrayCopy ()
]));
2023-03-06 14:24:02 +00:00
2023-09-27 15:51:17 +00:00
$queueForEvents -> setParam ( 'teamId' , $team -> getId ());
2023-03-06 14:24:02 +00:00
2025-09-04 12:06:38 +00:00
$response -> dynamic ( $prefs , Response :: MODEL_PREFERENCES );
2023-03-06 14:24:02 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/teams/:teamId' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Delete team' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].delete' )
2019-05-09 06:54:39 +00:00
-> label ( 'scope' , 'teams.write' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'team.delete' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'team/{request.teamId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'teams' ,
2025-01-17 04:31:39 +00:00
name : 'delete' ,
description : '/docs/references/teams/delete-team.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2024-05-29 19:52:22 +00:00
-> inject ( 'getProjectDB' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'queueForEvents' )
2024-05-29 19:52:22 +00:00
-> inject ( 'project' )
2024-08-08 14:03:10 +00:00
-> action ( function ( string $teamId , Response $response , callable $getProjectDB , Database $dbForProject , Delete $queueForDeletes , Event $queueForEvents , Document $project ) {
2019-05-09 06:54:39 +00:00
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2019-05-09 06:54:39 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2021-12-27 12:45:23 +00:00
if ( ! $dbForProject -> deleteDocument ( 'teams' , $teamId )) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed to remove team from DB' );
2019-05-09 06:54:39 +00:00
}
2020-06-30 11:09:28 +00:00
2025-12-29 08:04:47 +00:00
// Sync delete
2024-05-29 19:52:22 +00:00
$deletes = new Deletes ();
2024-06-27 22:27:04 +00:00
$deletes -> deleteMemberships ( $getProjectDB , $team , $project );
2025-12-29 12:24:26 +00:00
// Async delete
2024-06-27 22:27:04 +00:00
if ( $project -> getId () === 'console' ) {
$queueForDeletes
-> setType ( DELETE_TYPE_TEAM_PROJECTS )
2026-01-10 15:57:23 +00:00
-> setDocument ( $team )
2025-12-29 12:24:26 +00:00
-> trigger ();
2024-06-27 22:27:04 +00:00
}
2021-05-11 17:55:38 +00:00
2025-12-29 08:04:47 +00:00
$queueForDeletes
-> setType ( DELETE_TYPE_DOCUMENT )
2026-01-10 15:57:23 +00:00
-> setDocument ( $team );
2025-12-29 08:04:47 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-04-13 12:39:31 +00:00
-> setParam ( 'teamId' , $team -> getId ())
-> setPayload ( $response -> output ( $team , Response :: MODEL_TEAM ))
2020-12-02 22:15:20 +00:00
;
2020-06-30 11:09:28 +00:00
$response -> noContent ();
2020-12-26 16:48:43 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/teams/:teamId/memberships' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Create team membership' )
2021-02-28 18:36:13 +00:00
-> groups ([ 'api' , 'teams' , 'auth' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].memberships.[membershipId].create' )
2020-02-09 16:53:33 +00:00
-> label ( 'scope' , 'teams.write' )
2021-02-28 18:36:13 +00:00
-> label ( 'auth.type' , 'invites' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'membership.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'team/{request.teamId}' )
2022-08-12 11:01:12 +00:00
-> label ( 'audits.userId' , '{request.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'createMembership' ,
description : '/docs/references/teams/create-team-membership.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_MEMBERSHIP ,
)
]
))
2021-04-13 09:38:40 +00:00
-> label ( 'abuse-limit' , 10 )
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'Email of the new team member.' , true )
2023-01-18 03:48:47 +00:00
-> param ( 'userId' , '' , new UID (), 'ID of the user to be added to a team.' , true )
2023-01-09 05:46:02 +00:00
-> param ( 'phone' , '' , new Phone (), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.' , true )
2026-01-29 16:05:42 +00:00
-> param ( 'roles' , [], new ArrayList ( new Key (), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.' , false , [ 'project' ])
2025-12-07 20:29:45 +00:00
-> param ( 'url' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , true , [ 'redirectValidator' ]) // TODO add our own built-in confirm page
2021-12-10 15:44:54 +00:00
-> param ( 'name' , '' , new Text ( 128 ), 'Name of the new team member. Max length: 128 chars.' , true )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2020-12-26 16:48:43 +00:00
-> inject ( 'locale' )
2023-06-11 14:08:48 +00:00
-> inject ( 'queueForMails' )
2023-09-27 15:51:17 +00:00
-> inject ( 'queueForMessaging' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-01-08 11:31:01 +00:00
-> inject ( 'timelimit' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2025-01-08 11:31:01 +00:00
-> inject ( 'plan' )
2025-11-04 06:08:35 +00:00
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $teamId , string $email , string $userId , string $phone , array $roles , string $url , string $name , Response $response , Document $project , Document $user , Database $dbForProject , Authorization $authorization , Locale $locale , Mail $queueForMails , Messaging $queueForMessaging , Event $queueForEvents , callable $timelimit , StatsUsage $queueForStatsUsage , array $plan , Password $proofForPassword , Token $proofForToken ) {
$isAppUser = User :: isApp ( $authorization -> getRoles ());
$isPrivilegedUser = User :: isPrivileged ( $authorization -> getRoles ());
2023-09-05 18:13:47 +00:00
2024-07-22 13:37:28 +00:00
$url = htmlentities ( $url );
2023-09-05 18:13:47 +00:00
if ( empty ( $url )) {
2025-01-30 04:36:51 +00:00
if ( ! $isAppUser && ! $isPrivilegedUser ) {
2023-09-05 18:13:47 +00:00
throw new Exception ( Exception :: GENERAL_ARGUMENT_INVALID , 'URL is required' );
}
}
2020-11-20 06:48:15 +00:00
2023-01-09 05:55:07 +00:00
if ( empty ( $userId ) && empty ( $email ) && empty ( $phone )) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: GENERAL_ARGUMENT_INVALID , 'At least one of userId, email, or phone is required' );
}
2021-10-08 09:06:53 +00:00
2024-04-01 11:02:47 +00:00
if ( ! $isPrivilegedUser && ! $isAppUser && empty ( System :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED );
2022-05-27 05:45:38 +00:00
}
2021-06-03 19:05:11 +00:00
$email = \strtolower ( $email );
2025-02-20 17:36:42 +00:00
$name = empty ( $name ) ? $email : $name ;
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2020-06-30 11:09:28 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2023-01-09 05:55:07 +00:00
if ( ! empty ( $userId )) {
2023-01-09 05:46:02 +00:00
$invitee = $dbForProject -> getDocument ( 'users' , $userId );
2023-01-09 05:55:07 +00:00
if ( $invitee -> isEmpty ()) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND , 'User with given userId doesn\'t exist.' , 404 );
}
2023-04-11 15:32:14 +00:00
if ( ! empty ( $email ) && $invitee -> getAttribute ( 'email' , '' ) !== $email ) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS , 'Given userId and email doesn\'t match' , 409 );
}
2023-04-11 15:32:14 +00:00
if ( ! empty ( $phone ) && $invitee -> getAttribute ( 'phone' , '' ) !== $phone ) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS , 'Given userId and phone doesn\'t match' , 409 );
}
2023-01-09 09:17:39 +00:00
$email = $invitee -> getAttribute ( 'email' , '' );
$phone = $invitee -> getAttribute ( 'phone' , '' );
2025-02-20 17:36:42 +00:00
$name = $invitee -> getAttribute ( 'name' , '' ) ? : $name ;
2023-01-09 05:55:07 +00:00
} elseif ( ! empty ( $email )) {
2023-01-09 05:46:02 +00:00
$invitee = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'email' , [ $email ])]); // Get user by email address
2024-10-07 02:40:01 +00:00
if ( ! $invitee -> isEmpty () && ! empty ( $phone ) && $invitee -> getAttribute ( 'phone' , '' ) !== $phone ) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS , 'Given email and phone doesn\'t match' , 409 );
}
2023-01-09 05:55:07 +00:00
} elseif ( ! empty ( $phone )) {
2023-01-09 05:46:02 +00:00
$invitee = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'phone' , [ $phone ])]);
2024-10-07 02:40:01 +00:00
if ( ! $invitee -> isEmpty () && ! empty ( $email ) && $invitee -> getAttribute ( 'email' , '' ) !== $email ) {
2023-01-09 05:46:02 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS , 'Given phone and email doesn\'t match' , 409 );
}
}
2020-06-30 11:09:28 +00:00
2024-10-07 02:40:01 +00:00
if ( $invitee -> isEmpty ()) { // Create new user if no user with same email found
2021-08-06 08:34:17 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-10-08 09:06:53 +00:00
2024-05-16 02:30:46 +00:00
if ( ! $isPrivilegedUser && ! $isAppUser && $limit !== 0 && $project -> getId () !== 'console' ) { // check users limit, console invites are allways allowed.
2022-02-27 09:57:09 +00:00
$total = $dbForProject -> count ( 'users' , [], APP_LIMIT_USERS );
2021-10-08 09:06:53 +00:00
2022-05-23 14:54:50 +00:00
if ( $total >= $limit ) {
2022-08-14 06:56:12 +00:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED , 'Project registration is restricted. Contact your administrator for more information.' );
2021-02-28 18:36:13 +00:00
}
}
2023-05-18 01:11:45 +00:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2024-10-31 08:13:23 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 01:11:45 +00:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
2020-06-30 11:09:28 +00:00
try {
2022-08-14 14:22:38 +00:00
$userId = ID :: unique ();
2025-11-04 06:08:35 +00:00
$hash = $proofForPassword -> hash ( $proofForPassword -> generate ());
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-02 08:59:28 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-02 08:59:28 +00:00
}
2025-11-19 03:21:06 +00:00
$userId = ID :: unique ();
$userDocument = new Document ([
'$id' => $userId ,
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: read ( Role :: user ( $userId )),
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
],
'email' => empty ( $email ) ? null : $email ,
'phone' => empty ( $phone ) ? null : $phone ,
'emailVerification' => false ,
'status' => true ,
// TODO: Set password empty?
2025-11-23 06:43:52 +00:00
'password' => $hash ,
'hash' => $proofForPassword -> getHash () -> getName (),
'hashOptions' => $proofForPassword -> getHash () -> getOptions (),
2025-11-19 03:21:06 +00:00
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => null ,
'registration' => DateTime :: now (),
'reset' => false ,
'name' => $name ,
'prefs' => new \stdClass (),
'sessions' => null ,
'tokens' => null ,
'memberships' => null ,
'search' => implode ( ' ' , [ $userId , $email , $name ]),
'emailCanonical' => $emailCanonical ? -> getCanonical (),
'emailIsCanonical' => $emailCanonical ? -> isCanonicalSupported (),
'emailIsCorporate' => $emailCanonical ? -> isCorporate (),
'emailIsDisposable' => $emailCanonical ? -> isDisposable (),
'emailIsFree' => $emailCanonical ? -> isFree (),
]);
2020-06-30 11:09:28 +00:00
try {
2026-01-14 15:08:00 +00:00
$invitee = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $userDocument ));
2020-06-30 11:09:28 +00:00
} catch ( Duplicate $th ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS );
2019-05-09 06:54:39 +00:00
}
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2026-01-14 15:08:00 +00:00
$isOwner = $authorization -> hasRole ( 'team:' . $team -> getId () . '/owner' );
2019-05-09 06:54:39 +00:00
2021-03-01 21:04:53 +00:00
if ( ! $isOwner && ! $isPrivilegedUser && ! $isAppUser ) { // Not owner, not admin, not app (server)
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_UNAUTHORIZED , 'User is not allowed to send invitations for this team' );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-01-13 18:25:48 +00:00
$membership = $dbForProject -> findOne ( 'memberships' , [
2025-05-26 05:42:11 +00:00
Query :: equal ( 'userInternalId' , [ $invitee -> getSequence ()]),
Query :: equal ( 'teamInternalId' , [ $team -> getSequence ()]),
2020-06-30 11:09:28 +00:00
]);
2025-11-04 06:08:35 +00:00
$secret = $proofForToken -> generate ();
2025-01-13 18:25:48 +00:00
if ( $membership -> isEmpty ()) {
$membershipId = ID :: unique ();
$membership = new Document ([
'$id' => $membershipId ,
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: user ( $invitee -> getId ())),
Permission :: update ( Role :: team ( $team -> getId (), 'owner' )),
Permission :: delete ( Role :: user ( $invitee -> getId ())),
Permission :: delete ( Role :: team ( $team -> getId (), 'owner' )),
],
'userId' => $invitee -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $invitee -> getSequence (),
2025-01-13 18:25:48 +00:00
'teamId' => $team -> getId (),
2025-05-26 05:42:11 +00:00
'teamInternalId' => $team -> getSequence (),
2025-01-13 18:25:48 +00:00
'roles' => $roles ,
'invited' => DateTime :: now (),
'joined' => ( $isPrivilegedUser || $isAppUser ) ? DateTime :: now () : null ,
'confirm' => ( $isPrivilegedUser || $isAppUser ),
2025-11-04 06:08:35 +00:00
'secret' => $proofForToken -> hash ( $secret ),
2025-01-13 18:25:48 +00:00
'search' => implode ( ' ' , [ $membershipId , $invitee -> getId ()])
]);
2024-02-12 01:18:19 +00:00
2025-01-14 10:13:00 +00:00
$membership = ( $isPrivilegedUser || $isAppUser ) ?
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> createDocument ( 'memberships' , $membership )) :
2025-01-14 10:13:00 +00:00
$dbForProject -> createDocument ( 'memberships' , $membership );
2025-03-21 15:59:30 +00:00
if ( $isPrivilegedUser || $isAppUser ) {
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> increaseDocumentAttribute ( 'teams' , $team -> getId (), 'total' , 1 ));
2025-03-21 15:59:30 +00:00
}
2025-02-13 13:16:29 +00:00
} elseif ( $membership -> getAttribute ( 'confirm' ) === false ) {
2025-11-04 06:08:35 +00:00
$membership -> setAttribute ( 'secret' , $proofForToken -> hash ( $secret ));
2025-01-17 17:27:54 +00:00
$membership -> setAttribute ( 'invited' , DateTime :: now ());
if ( $isPrivilegedUser || $isAppUser ) {
$membership -> setAttribute ( 'joined' , DateTime :: now ());
$membership -> setAttribute ( 'confirm' , true );
2021-05-09 18:37:47 +00:00
}
2019-05-09 06:54:39 +00:00
2025-01-14 10:13:00 +00:00
$membership = ( $isPrivilegedUser || $isAppUser ) ?
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'memberships' , $membership -> getId (), $membership )) :
2025-01-13 18:51:23 +00:00
$dbForProject -> updateDocument ( 'memberships' , $membership -> getId (), $membership );
2025-02-12 09:50:13 +00:00
} else {
2025-02-13 13:16:29 +00:00
throw new Exception ( Exception :: MEMBERSHIP_ALREADY_CONFIRMED );
2025-01-14 10:13:00 +00:00
}
2025-01-13 18:51:23 +00:00
2025-01-14 10:43:32 +00:00
if ( $isPrivilegedUser || $isAppUser ) {
2025-01-14 10:17:05 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $invitee -> getId ());
} else {
2023-01-09 08:29:33 +00:00
$url = Template :: parseURL ( $url );
2025-01-25 17:25:07 +00:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'membershipId' => $membership -> getId (), 'userId' => $invitee -> getId (), 'secret' => $secret , 'teamId' => $teamId , 'teamName' => $team -> getAttribute ( 'name' )]);
2023-01-09 08:29:33 +00:00
$url = Template :: unParseURL ( $url );
2023-01-13 06:05:16 +00:00
if ( ! empty ( $email )) {
$projectName = $project -> isEmpty () ? 'Console' : $project -> getAttribute ( 'name' , '[APP-NAME]' );
2023-08-28 05:53:02 +00:00
$body = $locale -> getText ( " emails.invitation.body " );
2025-07-22 11:13:17 +00:00
$preview = $locale -> getText ( " emails.invitation.preview " );
2025-08-27 11:43:10 +00:00
$subject = $locale -> getText ( " emails.invitation.subject " );
2023-04-19 08:29:29 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.invitation-' . $locale -> default ] ? ? [];
2023-08-28 05:53:26 +00:00
2023-08-29 09:40:30 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-inner-base.tpl' );
2023-10-04 23:14:27 +00:00
$message
2024-01-08 17:08:17 +00:00
-> setParam ( '{{body}}' , $body , escapeHtml : false )
2023-10-04 23:14:27 +00:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.invitation.hello " ))
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.invitation.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.invitation.thanks " ))
2025-03-28 11:14:44 +00:00
-> setParam ( '{{buttonText}}' , $locale -> getText ( " emails.invitation.buttonText " ))
2023-10-04 23:14:27 +00:00
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.invitation.signature " ));
2023-08-29 09:40:30 +00:00
$body = $message -> render ();
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
2023-08-28 12:21:35 +00:00
2024-04-01 11:02:47 +00:00
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
2023-08-29 09:40:30 +00:00
$replyTo = " " ;
2023-08-28 12:21:35 +00:00
2023-08-29 09:40:30 +00:00
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
}
2023-08-28 12:19:37 +00:00
2023-10-01 17:39:26 +00:00
$queueForMails
2023-08-25 15:13:25 +00:00
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
2023-08-29 09:40:30 +00:00
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
2023-08-28 12:19:37 +00:00
2023-08-30 05:31:02 +00:00
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
}
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
2023-08-29 09:40:30 +00:00
}
2023-09-27 15:51:17 +00:00
$queueForMails
2023-08-30 05:31:02 +00:00
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
2023-04-19 08:29:29 +00:00
}
2023-08-28 05:53:02 +00:00
$emailVariables = [
'owner' => $user -> getAttribute ( 'name' ),
'direction' => $locale -> getText ( 'settings.direction' ),
2024-01-11 20:36:05 +00:00
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
2025-01-16 04:29:11 +00:00
'user' => $name ,
2023-08-30 21:48:25 +00:00
'team' => $team -> getAttribute ( 'name' ),
2024-01-11 20:36:05 +00:00
'redirect' => $url ,
2024-01-08 17:08:17 +00:00
'project' => $projectName
2023-08-28 05:53:02 +00:00
];
2023-01-13 06:05:16 +00:00
2023-09-27 15:51:17 +00:00
$queueForMails
2023-01-13 06:05:16 +00:00
-> setSubject ( $subject )
-> setBody ( $body )
2025-07-22 11:13:17 +00:00
-> setPreview ( $preview )
2023-01-13 06:05:16 +00:00
-> setRecipient ( $invitee -> getAttribute ( 'email' ))
2025-02-21 03:27:46 +00:00
-> setName ( $invitee -> getAttribute ( 'name' , '' ))
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2025-01-14 10:17:05 +00:00
-> trigger ();
2023-01-13 06:05:16 +00:00
} elseif ( ! empty ( $phone )) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMS_PROVIDER' ))) {
2023-09-05 17:10:48 +00:00
throw new Exception ( Exception :: GENERAL_PHONE_DISABLED , 'Phone provider not configured' );
}
2023-03-14 09:07:42 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/sms-base.tpl' );
2023-04-19 08:29:29 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'sms.invitation-' . $locale -> default ] ? ? [];
2023-04-19 08:44:22 +00:00
if ( ! empty ( $customTemplate )) {
2023-04-19 08:29:29 +00:00
$message = $customTemplate [ 'message' ];
}
2023-03-17 00:55:00 +00:00
$message = $message -> setParam ( '{{token}}' , $url );
2023-03-14 09:07:42 +00:00
$message = $message -> render ();
2023-11-15 20:00:47 +00:00
$messageDoc = new Document ([
'$id' => ID :: unique (),
2023-09-05 17:10:48 +00:00
'data' => [
'content' => $message ,
],
2023-11-15 20:00:47 +00:00
]);
2023-09-05 17:10:48 +00:00
2023-09-27 15:51:17 +00:00
$queueForMessaging
2024-02-20 12:06:35 +00:00
-> setType ( MESSAGE_SEND_TYPE_INTERNAL )
2023-11-15 20:00:47 +00:00
-> setMessage ( $messageDoc )
-> setRecipients ([ $phone ])
2024-02-20 12:06:35 +00:00
-> setProviderType ( 'SMS' );
2025-01-08 11:31:01 +00:00
2026-01-15 12:59:30 +00:00
$helper = PhoneNumberUtil :: getInstance ();
2026-01-15 13:19:34 +00:00
try {
$countryCode = $helper -> parse ( $phone ) -> getCountryCode ();
2025-01-08 11:31:01 +00:00
2026-01-15 13:19:34 +00:00
if ( ! empty ( $countryCode )) {
$queueForStatsUsage
-> addMetric ( str_replace ( '{countryCode}' , $countryCode , METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE ), 1 );
}
} catch ( NumberParseException $e ) {
// Ignore invalid phone number for country code stats
2025-01-08 11:31:01 +00:00
}
2026-01-15 12:59:30 +00:00
$queueForStatsUsage
-> addMetric ( METRIC_AUTH_METHOD_PHONE , 1 )
-> setProject ( $project )
-> trigger ();
2023-01-13 06:05:16 +00:00
}
2019-05-09 06:54:39 +00:00
}
2022-12-20 16:11:30 +00:00
$queueForEvents
2024-07-19 00:02:43 +00:00
-> setParam ( 'userId' , $invitee -> getId ())
2022-04-13 12:39:31 +00:00
-> setParam ( 'teamId' , $team -> getId ())
-> setParam ( 'membershipId' , $membership -> getId ())
2020-06-30 11:09:28 +00:00
;
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic (
$membership
-> setAttribute ( 'teamName' , $team -> getAttribute ( 'name' ))
2025-02-21 03:27:46 +00:00
-> setAttribute ( 'userName' , $invitee -> getAttribute ( 'name' ))
2022-09-07 11:23:57 +00:00
-> setAttribute ( 'userEmail' , $invitee -> getAttribute ( 'email' )),
2022-09-07 11:14:53 +00:00
Response :: MODEL_MEMBERSHIP
);
2020-12-26 16:48:43 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams/:teamId/memberships' )
2023-08-01 15:26:48 +00:00
-> desc ( 'List team memberships' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2020-01-31 22:34:07 +00:00
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'listMemberships' ,
description : '/docs/references/teams/list-team-members.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_MEMBERSHIP_LIST ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2023-03-29 19:38:39 +00:00
-> param ( 'queries' , [], new Memberships (), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode ( ', ' , Memberships :: ALLOWED_ATTRIBUTES ), true )
2020-09-10 14:40:14 +00:00
-> param ( 'search' , '' , new Text ( 256 ), 'Search term to filter your list results. Max length: 256 chars.' , true )
2025-11-05 02:39:03 +00:00
-> param ( 'total' , true , new Boolean ( true ), 'When set to false, the total count returned will be 0 and will not be calculated.' , true )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2024-11-01 11:38:56 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $teamId , array $queries , string $search , bool $includeTotal , Response $response , Document $project , Database $dbForProject , Authorization $authorization ) {
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2020-01-31 22:34:07 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2020-01-31 22:34:07 +00:00
2024-02-12 16:02:04 +00:00
try {
$queries = Query :: parseQueries ( $queries );
} catch ( QueryException $e ) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $e -> getMessage ());
}
2022-08-11 23:53:52 +00:00
if ( ! empty ( $search )) {
2022-08-23 13:16:46 +00:00
$queries [] = Query :: search ( 'search' , $search );
2022-08-11 23:53:52 +00:00
}
2022-08-23 13:16:46 +00:00
// Set internal queries
2025-05-26 05:42:11 +00:00
$queries [] = Query :: equal ( 'teamInternalId' , [ $team -> getSequence ()]);
2022-08-23 13:16:46 +00:00
2026-01-28 12:53:24 +00:00
$cursor = Query :: getCursorQueries ( $queries , false );
$cursor = \reset ( $cursor );
2024-10-17 05:41:24 +00:00
2026-01-28 12:53:24 +00:00
if ( $cursor !== false ) {
2024-10-17 05:41:24 +00:00
$validator = new Cursor ();
if ( ! $validator -> isValid ( $cursor )) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $validator -> getDescription ());
}
2022-08-23 13:16:46 +00:00
$membershipId = $cursor -> getValue ();
$cursorDocument = $dbForProject -> getDocument ( 'memberships' , $membershipId );
2021-08-06 12:36:35 +00:00
2022-08-11 23:53:52 +00:00
if ( $cursorDocument -> isEmpty ()) {
2022-08-23 13:16:46 +00:00
throw new Exception ( Exception :: GENERAL_CURSOR_NOT_FOUND , " Membership ' { $membershipId } ' for the 'cursor' value not found. " );
2021-08-06 12:36:35 +00:00
}
2022-02-16 16:26:19 +00:00
2022-08-23 13:16:46 +00:00
$cursor -> setValue ( $cursorDocument );
2022-02-16 16:26:19 +00:00
}
2022-08-23 13:16:46 +00:00
$filterQueries = Query :: groupByType ( $queries )[ 'filters' ];
2025-04-16 11:39:53 +00:00
try {
$memberships = $dbForProject -> find (
collection : 'memberships' ,
queries : $queries ,
);
2025-11-05 02:39:03 +00:00
$total = $includeTotal ? $dbForProject -> count (
2025-04-17 04:46:26 +00:00
collection : 'memberships' ,
queries : $filterQueries ,
max : APP_LIMIT_COUNT
2025-11-05 02:39:03 +00:00
) : 0 ;
2025-04-16 11:39:53 +00:00
} catch ( OrderException $e ) {
2025-04-17 04:46:26 +00:00
throw new Exception ( Exception :: DATABASE_QUERY_ORDER_NULL , " The order attribute ' { $e -> getAttribute () } ' had a null value. Cursor pagination requires all documents order attribute values are non-null. " );
2025-04-16 11:39:53 +00:00
}
2022-08-23 13:16:46 +00:00
2020-06-30 11:09:28 +00:00
2025-11-04 07:12:42 +00:00
$memberships = array_filter ( $memberships , fn ( Document $membership ) => ! empty ( $membership -> getAttribute ( 'userId' )));
2020-06-30 11:09:28 +00:00
2024-11-07 09:19:22 +00:00
$membershipsPrivacy = [
2024-11-07 10:13:20 +00:00
'userName' => $project -> getAttribute ( 'auths' , [])[ 'membershipsUserName' ] ? ? true ,
'userEmail' => $project -> getAttribute ( 'auths' , [])[ 'membershipsUserEmail' ] ? ? true ,
'mfa' => $project -> getAttribute ( 'auths' , [])[ 'membershipsMfa' ] ? ? true ,
2024-11-06 20:15:31 +00:00
];
2026-01-14 15:08:00 +00:00
$roles = $authorization -> getRoles ();
2025-11-04 07:12:34 +00:00
$isPrivilegedUser = User :: isPrivileged ( $roles );
$isAppUser = User :: isApp ( $roles );
2024-11-05 18:46:22 +00:00
2024-11-06 20:15:31 +00:00
$membershipsPrivacy = array_map ( function ( $privacy ) use ( $isPrivilegedUser , $isAppUser ) {
return $privacy || $isPrivilegedUser || $isAppUser ;
}, $membershipsPrivacy );
2024-11-01 11:38:56 +00:00
2024-11-06 20:15:31 +00:00
$memberships = array_map ( function ( $membership ) use ( $dbForProject , $team , $membershipsPrivacy ) {
2024-11-13 09:45:50 +00:00
$user = ! empty ( array_filter ( $membershipsPrivacy ))
? $dbForProject -> getDocument ( 'users' , $membership -> getAttribute ( 'userId' ))
: new Document ();
2024-11-06 20:15:31 +00:00
if ( $membershipsPrivacy [ 'mfa' ]) {
2024-11-01 11:38:56 +00:00
$mfa = $user -> getAttribute ( 'mfa' , false );
2024-11-06 20:15:31 +00:00
2024-11-01 11:38:56 +00:00
if ( $mfa ) {
$totp = TOTP :: getAuthenticatorFromUser ( $user );
$totpEnabled = $totp && $totp -> getAttribute ( 'verified' , false );
$emailEnabled = $user -> getAttribute ( 'email' , false ) && $user -> getAttribute ( 'emailVerification' , false );
$phoneEnabled = $user -> getAttribute ( 'phone' , false ) && $user -> getAttribute ( 'phoneVerification' , false );
if ( ! $totpEnabled && ! $emailEnabled && ! $phoneEnabled ) {
$mfa = false ;
}
2024-11-05 18:46:22 +00:00
}
2024-11-05 17:37:48 +00:00
2024-11-06 20:15:31 +00:00
$membership -> setAttribute ( 'mfa' , $mfa );
}
if ( $membershipsPrivacy [ 'userName' ]) {
$membership -> setAttribute ( 'userName' , $user -> getAttribute ( 'name' ));
}
if ( $membershipsPrivacy [ 'userEmail' ]) {
$membership -> setAttribute ( 'userEmail' , $user -> getAttribute ( 'email' ));
2024-11-01 11:38:56 +00:00
}
2021-12-17 15:11:22 +00:00
2024-11-05 18:46:22 +00:00
$membership -> setAttribute ( 'teamName' , $team -> getAttribute ( 'name' ));
2024-11-06 20:15:31 +00:00
2021-12-17 15:11:22 +00:00
return $membership ;
}, $memberships );
2020-06-30 11:09:28 +00:00
2021-07-25 14:47:18 +00:00
$response -> dynamic ( new Document ([
2021-12-17 15:11:22 +00:00
'memberships' => $memberships ,
2022-02-27 09:57:09 +00:00
'total' => $total ,
2021-05-06 22:31:05 +00:00
]), Response :: MODEL_MEMBERSHIP_LIST );
2020-12-26 16:48:43 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams/:teamId/memberships/:membershipId' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Get team membership' )
2021-08-04 06:57:30 +00:00
-> groups ([ 'api' , 'teams' ])
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'getMembership' ,
description : '/docs/references/teams/get-team-member.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_MEMBERSHIP ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
-> param ( 'membershipId' , '' , new UID (), 'Membership ID.' )
2021-08-04 06:57:30 +00:00
-> inject ( 'response' )
2024-11-05 18:46:22 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $teamId , string $membershipId , Response $response , Document $project , Database $dbForProject , Authorization $authorization ) {
2021-08-04 06:57:30 +00:00
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2021-08-04 06:57:30 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2021-08-04 06:57:30 +00:00
}
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> getDocument ( 'memberships' , $membershipId );
2021-08-04 06:57:30 +00:00
2022-05-23 14:54:50 +00:00
if ( $membership -> isEmpty () || empty ( $membership -> getAttribute ( 'userId' ))) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: MEMBERSHIP_NOT_FOUND );
2021-08-04 06:57:30 +00:00
}
2024-11-07 09:19:22 +00:00
$membershipsPrivacy = [
2024-11-07 10:13:20 +00:00
'userName' => $project -> getAttribute ( 'auths' , [])[ 'membershipsUserName' ] ? ? true ,
'userEmail' => $project -> getAttribute ( 'auths' , [])[ 'membershipsUserEmail' ] ? ? true ,
'mfa' => $project -> getAttribute ( 'auths' , [])[ 'membershipsMfa' ] ? ? true ,
2024-11-06 20:15:31 +00:00
];
2026-01-14 15:08:00 +00:00
$roles = $authorization -> getRoles ();
2025-11-05 02:39:03 +00:00
$isPrivilegedUser = User :: isPrivileged ( $roles );
$isAppUser = User :: isApp ( $roles );
2021-12-17 13:59:55 +00:00
2024-11-06 20:15:31 +00:00
$membershipsPrivacy = array_map ( function ( $privacy ) use ( $isPrivilegedUser , $isAppUser ) {
return $privacy || $isPrivilegedUser || $isAppUser ;
}, $membershipsPrivacy );
2024-02-25 16:35:33 +00:00
2024-11-13 09:45:50 +00:00
$user = ! empty ( array_filter ( $membershipsPrivacy ))
? $dbForProject -> getDocument ( 'users' , $membership -> getAttribute ( 'userId' ))
: new Document ();
2024-02-25 16:35:33 +00:00
2024-11-13 09:45:50 +00:00
if ( $membershipsPrivacy [ 'mfa' ]) {
2024-11-05 18:46:22 +00:00
$mfa = $user -> getAttribute ( 'mfa' , false );
if ( $mfa ) {
$totp = TOTP :: getAuthenticatorFromUser ( $user );
$totpEnabled = $totp && $totp -> getAttribute ( 'verified' , false );
$emailEnabled = $user -> getAttribute ( 'email' , false ) && $user -> getAttribute ( 'emailVerification' , false );
$phoneEnabled = $user -> getAttribute ( 'phone' , false ) && $user -> getAttribute ( 'phoneVerification' , false );
if ( ! $totpEnabled && ! $emailEnabled && ! $phoneEnabled ) {
$mfa = false ;
}
2024-02-25 16:35:33 +00:00
}
2024-11-06 15:00:27 +00:00
2024-11-06 20:15:31 +00:00
$membership -> setAttribute ( 'mfa' , $mfa );
}
if ( $membershipsPrivacy [ 'userName' ]) {
$membership -> setAttribute ( 'userName' , $user -> getAttribute ( 'name' ));
}
if ( $membershipsPrivacy [ 'userEmail' ]) {
$membership -> setAttribute ( 'userEmail' , $user -> getAttribute ( 'email' ));
2024-02-25 16:35:33 +00:00
}
2024-11-05 18:46:22 +00:00
$membership -> setAttribute ( 'teamName' , $team -> getAttribute ( 'name' ));
2021-08-04 06:57:30 +00:00
2022-05-23 14:54:50 +00:00
$response -> dynamic ( $membership , Response :: MODEL_MEMBERSHIP );
2021-08-04 06:57:30 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/teams/:teamId/memberships/:membershipId' )
2023-09-27 15:11:58 +00:00
-> desc ( 'Update membership' )
2021-05-12 13:44:41 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].memberships.[membershipId].update' )
2021-05-13 14:47:35 +00:00
-> label ( 'scope' , 'teams.write' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'membership.update' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'team/{request.teamId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'updateMembership' ,
description : '/docs/references/teams/update-team-membership.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_MEMBERSHIP ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2021-05-12 13:44:41 +00:00
-> param ( 'membershipId' , '' , new UID (), 'Membership ID.' )
2026-01-29 16:05:42 +00:00
-> param ( 'roles' , [], new ArrayList ( new Key (), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.' , false , [ 'project' ])
2021-05-12 13:44:41 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
2025-04-05 13:50:24 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $teamId , string $membershipId , array $roles , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Authorization $authorization , Event $queueForEvents ) {
2021-05-12 13:44:41 +00:00
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2021-05-15 01:20:12 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2021-05-12 14:47:56 +00:00
}
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> getDocument ( 'memberships' , $membershipId );
2021-05-15 01:20:12 +00:00
if ( $membership -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: MEMBERSHIP_NOT_FOUND );
2021-05-12 13:44:41 +00:00
}
2021-12-27 12:45:23 +00:00
$profile = $dbForProject -> getDocument ( 'users' , $membership -> getAttribute ( 'userId' ));
2021-05-15 01:20:12 +00:00
if ( $profile -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2021-05-15 01:20:12 +00:00
}
2021-05-12 13:44:41 +00:00
2026-01-14 15:08:00 +00:00
$isPrivilegedUser = User :: isPrivileged ( $authorization -> getRoles ());
$isAppUser = User :: isApp ( $authorization -> getRoles ());
$isOwner = $authorization -> hasRole ( 'team:' . $team -> getId () . '/owner' );
2021-12-17 13:59:55 +00:00
2025-04-05 13:50:24 +00:00
if ( $project -> getId () === 'console' ) {
// Quick check: fetch up to 2 owners to determine if only one exists
$ownersCount = $dbForProject -> count (
collection : 'memberships' ,
2025-05-15 08:38:54 +00:00
queries : [
Query :: contains ( 'roles' , [ 'owner' ]),
2025-05-26 05:42:11 +00:00
Query :: equal ( 'teamInternalId' , [ $team -> getSequence ()])
2025-05-15 08:38:54 +00:00
],
2025-04-05 13:50:24 +00:00
max : 2
);
2025-05-26 05:59:46 +00:00
// Is the role change being requested by the user on their own membership?
2025-06-10 00:50:02 +00:00
$isCurrentUserAnOwner = $user -> getSequence () === $membership -> getAttribute ( 'userInternalId' );
2025-05-26 05:59:46 +00:00
2025-04-05 14:12:20 +00:00
// Prevent role change if there's only one owner left,
2025-05-26 05:59:46 +00:00
// the requester is that owner, and the new `$roles` no longer include 'owner'
if ( $ownersCount === 1 && $isOwner && $isCurrentUserAnOwner && ! \in_array ( 'owner' , $roles )) {
2025-05-30 08:38:25 +00:00
throw new Exception ( Exception :: MEMBERSHIP_DOWNGRADE_PROHIBITED , 'There must be at least one owner in the organization.' );
2025-04-05 13:50:24 +00:00
}
}
2021-05-12 17:00:22 +00:00
if ( ! $isOwner && ! $isPrivilegedUser && ! $isAppUser ) { // Not owner, not admin, not app (server)
2022-08-14 08:55:59 +00:00
throw new Exception ( Exception :: USER_UNAUTHORIZED , 'User is not allowed to modify roles' );
2021-05-12 17:00:22 +00:00
}
2021-05-12 13:44:41 +00:00
2022-02-16 16:26:19 +00:00
/**
* Update the roles
*/
2021-05-12 17:00:22 +00:00
$membership -> setAttribute ( 'roles' , $roles );
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> updateDocument ( 'memberships' , $membership -> getId (), $membership );
2021-05-12 13:44:41 +00:00
2022-02-16 16:26:19 +00:00
/**
* Replace membership on profile
*/
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2021-05-12 13:44:41 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2024-07-19 00:02:43 +00:00
-> setParam ( 'userId' , $profile -> getId ())
2022-04-13 12:39:31 +00:00
-> setParam ( 'teamId' , $team -> getId ())
-> setParam ( 'membershipId' , $membership -> getId ());
2021-05-12 13:44:41 +00:00
2022-02-16 16:26:19 +00:00
$response -> dynamic (
$membership
2022-05-12 13:20:06 +00:00
-> setAttribute ( 'teamName' , $team -> getAttribute ( 'name' ))
-> setAttribute ( 'userName' , $profile -> getAttribute ( 'name' ))
-> setAttribute ( 'userEmail' , $profile -> getAttribute ( 'email' )),
2022-02-16 16:26:19 +00:00
Response :: MODEL_MEMBERSHIP
);
2020-12-26 16:48:43 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/teams/:teamId/memberships/:membershipId/status' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update team membership status' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].memberships.[membershipId].update.status' )
2020-01-19 12:22:54 +00:00
-> label ( 'scope' , 'public' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'membership.update' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'team/{request.teamId}' )
2022-08-12 11:01:12 +00:00
-> label ( 'audits.userId' , '{request.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'updateMembershipStatus' ,
description : '/docs/references/teams/update-team-membership-status.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_MEMBERSHIP ,
)
]
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2021-05-07 15:49:23 +00:00
-> param ( 'membershipId' , '' , new UID (), 'Membership ID.' )
2021-12-10 12:27:11 +00:00
-> param ( 'userId' , '' , new UID (), 'User ID.' )
2020-09-10 14:40:14 +00:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Secret key.' )
2020-12-26 16:48:43 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2022-10-31 14:54:15 +00:00
-> inject ( 'project' )
2020-12-26 16:48:43 +00:00
-> inject ( 'geodb' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 06:08:35 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $teamId , string $membershipId , string $userId , string $secret , Request $request , Response $response , Document $user , Database $dbForProject , Authorization $authorization , $project , Reader $geodb , Event $queueForEvents , Store $store , Token $proofForToken ) {
2020-06-30 11:09:28 +00:00
$protocol = $request -> getProtocol ();
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> getDocument ( 'memberships' , $membershipId );
2020-06-30 11:09:28 +00:00
2021-06-20 13:59:36 +00:00
if ( $membership -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: MEMBERSHIP_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2026-01-14 15:08:00 +00:00
$team = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'teams' , $teamId ));
2019-05-09 06:54:39 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-05-26 05:42:11 +00:00
if ( $membership -> getAttribute ( 'teamInternalId' ) !== $team -> getSequence ()) {
2024-03-17 09:23:43 +00:00
throw new Exception ( Exception :: TEAM_MEMBERSHIP_MISMATCH );
}
2025-11-04 06:08:35 +00:00
if ( ! $proofForToken -> verify ( $secret , $membership -> getAttribute ( 'secret' ))) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_INVALID_SECRET );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2022-04-13 12:39:31 +00:00
if ( $userId !== $membership -> getAttribute ( 'userId' )) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_INVITE_MISMATCH , 'Invite does not belong to current user (' . $user -> getAttribute ( 'email' ) . ')' );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2024-11-19 15:47:14 +00:00
$hasSession = ! $user -> isEmpty ();
if ( ! $hasSession ) {
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $dbForProject -> getDocument ( 'users' , $userId ) -> getArrayCopy ()); // Get user
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-05-26 05:42:11 +00:00
if ( $membership -> getAttribute ( 'userInternalId' ) !== $user -> getSequence ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_INVITE_MISMATCH , 'Invite does not belong to current user (' . $user -> getAttribute ( 'email' ) . ')' );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2022-01-18 22:27:24 +00:00
if ( $membership -> getAttribute ( 'confirm' ) === true ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: MEMBERSHIP_ALREADY_CONFIRMED );
2022-01-18 22:27:24 +00:00
}
2020-06-30 11:09:28 +00:00
$membership // Attach user to team
2022-07-13 14:02:49 +00:00
-> setAttribute ( 'joined' , DateTime :: now ())
2020-06-30 11:09:28 +00:00
-> setAttribute ( 'confirm' , true )
;
2019-05-09 06:54:39 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user -> setAttribute ( 'emailVerification' , true )));
2019-05-09 06:54:39 +00:00
2024-11-19 15:47:14 +00:00
// Create session for the user if not logged in
if ( ! $hasSession ) {
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2024-11-19 15:47:14 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2025-11-04 06:08:35 +00:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2024-11-19 15:47:14 +00:00
$expire = DateTime :: addSeconds ( new \DateTime (), $authDuration );
2025-11-04 06:08:35 +00:00
$secret = $proofForToken -> generate ();
2024-11-19 15:47:14 +00:00
$session = new Document ( array_merge ([
'$id' => ID :: unique (),
'$permissions' => [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 06:08:35 +00:00
'provider' => SESSION_PROVIDER_EMAIL ,
2024-11-19 15:47:14 +00:00
'providerUid' => $user -> getAttribute ( 'email' ),
2025-11-04 06:08:35 +00:00
'secret' => $proofForToken -> hash ( $secret ), // One way hash encryption to protect DB leak
2024-11-19 15:47:14 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'factors' => [ 'email' ],
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
'expire' => DateTime :: addSeconds ( new \DateTime (), $authDuration )
], $detector -> getOS (), $detector -> getClient (), $detector -> getDevice ()));
$session = $dbForProject -> createDocument ( 'sessions' , $session );
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $userId ) -> toString ());
2024-11-19 15:47:14 +00:00
2025-11-04 06:08:35 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $secret )
-> encode ();
2025-09-14 04:53:49 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2025-11-04 06:08:35 +00:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ $store -> getKey () => $encoded ]));
2024-11-19 15:47:14 +00:00
}
2019-05-09 06:54:39 +00:00
2024-11-19 15:47:14 +00:00
$response
-> addCookie (
2025-11-04 06:08:35 +00:00
name : $store -> getKey () . '_legacy' ,
value : $encoded ,
2024-11-19 15:47:14 +00:00
expire : ( new \DateTime ( $expire )) -> getTimestamp (),
2024-11-19 15:50:31 +00:00
path : '/' ,
2024-11-19 15:47:14 +00:00
domain : Config :: getParam ( 'cookieDomain' ),
secure : ( 'https' === $protocol ),
2024-11-19 15:50:31 +00:00
httponly : true
2024-11-19 15:47:14 +00:00
)
-> addCookie (
2025-11-04 06:08:35 +00:00
name : $store -> getKey (),
value : $encoded ,
2024-11-19 15:47:14 +00:00
expire : ( new \DateTime ( $expire )) -> getTimestamp (),
2024-11-19 15:50:31 +00:00
path : '/' ,
domain : Config :: getParam ( 'cookieDomain' ),
secure : ( 'https' === $protocol ),
httponly : true ,
2024-11-19 15:47:14 +00:00
sameSite : Config :: getParam ( 'cookieSamesite' )
)
;
}
2019-05-09 06:54:39 +00:00
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> updateDocument ( 'memberships' , $membership -> getId (), $membership );
2022-05-12 13:20:06 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-01-19 12:22:54 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> increaseDocumentAttribute ( 'teams' , $team -> getId (), 'total' , 1 ));
2019-05-09 06:54:39 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2024-07-19 00:02:43 +00:00
-> setParam ( 'userId' , $user -> getId ())
2022-04-13 12:39:31 +00:00
-> setParam ( 'teamId' , $team -> getId ())
-> setParam ( 'membershipId' , $membership -> getId ())
2020-06-30 11:09:28 +00:00
;
2020-03-17 11:36:13 +00:00
2022-05-23 14:54:50 +00:00
$response -> dynamic (
$membership
2024-11-19 15:47:14 +00:00
-> setAttribute ( 'teamName' , $team -> getAttribute ( 'name' ))
-> setAttribute ( 'userName' , $user -> getAttribute ( 'name' ))
-> setAttribute ( 'userEmail' , $user -> getAttribute ( 'email' )),
2022-05-23 14:54:50 +00:00
Response :: MODEL_MEMBERSHIP
);
2020-12-26 16:48:43 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/teams/:teamId/memberships/:membershipId' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Delete team membership' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'teams' ])
2022-04-13 12:39:31 +00:00
-> label ( 'event' , 'teams.[teamId].memberships.[membershipId].delete' )
2020-02-09 16:53:33 +00:00
-> label ( 'scope' , 'teams.write' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'membership.delete' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'team/{request.teamId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'memberships' ,
2025-01-17 04:31:39 +00:00
name : 'deleteMembership' ,
description : '/docs/references/teams/delete-team-membership.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: KEY , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2021-12-10 12:27:11 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2021-05-07 15:49:23 +00:00
-> param ( 'membershipId' , '' , new UID (), 'Membership ID.' )
2025-05-26 05:59:46 +00:00
-> inject ( 'user' )
2025-05-25 06:49:52 +00:00
-> inject ( 'project' )
2020-12-26 16:48:43 +00:00
-> inject ( 'response' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $teamId , string $membershipId , Document $user , Document $project , Response $response , Database $dbForProject , Authorization $authorization , Event $queueForEvents ) {
2019-05-09 06:54:39 +00:00
2021-12-27 12:45:23 +00:00
$membership = $dbForProject -> getDocument ( 'memberships' , $membershipId );
2019-05-09 06:54:39 +00:00
2021-06-20 13:59:36 +00:00
if ( $membership -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_INVITE_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-05-26 05:59:46 +00:00
$profile = $dbForProject -> getDocument ( 'users' , $membership -> getAttribute ( 'userId' ));
2021-05-09 21:20:57 +00:00
2025-05-26 05:59:46 +00:00
if ( $profile -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2021-05-09 21:20:57 +00:00
}
2021-12-27 12:45:23 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
2019-05-09 06:54:39 +00:00
2021-06-20 13:59:36 +00:00
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-05-26 05:42:11 +00:00
if ( $membership -> getAttribute ( 'teamInternalId' ) !== $team -> getSequence ()) {
2024-03-17 09:23:43 +00:00
throw new Exception ( Exception :: TEAM_MEMBERSHIP_MISMATCH );
}
2019-05-09 06:54:39 +00:00
2025-05-25 06:49:52 +00:00
if ( $project -> getId () === 'console' ) {
// Quick check:
// fetch up to 2 owners to determine if only one exists
$ownersCount = $dbForProject -> count (
collection : 'memberships' ,
queries : [
Query :: contains ( 'roles' , [ 'owner' ]),
2025-06-10 00:50:02 +00:00
Query :: equal ( 'teamInternalId' , [ $team -> getSequence ()])
2025-05-25 06:49:52 +00:00
],
max : 2
);
2025-05-30 12:34:57 +00:00
// Is the deletion being requested by the user on their own membership and they are also the owner?
$isSelfOwner =
in_array ( 'owner' , $membership -> getAttribute ( 'roles' )) &&
2025-06-10 00:50:02 +00:00
$membership -> getAttribute ( 'userInternalId' ) === $user -> getSequence ();
2025-05-26 05:59:46 +00:00
2025-05-30 12:34:57 +00:00
if ( $ownersCount === 1 && $isSelfOwner ) {
2025-05-25 06:49:52 +00:00
/* Prevent removal if the user is the only owner. */
2025-05-30 08:38:25 +00:00
throw new Exception ( Exception :: MEMBERSHIP_DELETION_PROHIBITED , 'There must be at least one owner in the organization.' );
2025-05-25 06:49:52 +00:00
}
}
2021-12-07 08:01:09 +00:00
try {
$dbForProject -> deleteDocument ( 'memberships' , $membership -> getId ());
} catch ( AuthorizationException $exception ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_UNAUTHORIZED );
2024-02-08 01:17:54 +00:00
} catch ( \Throwable $exception ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed to remove membership from DB' );
2020-06-30 11:09:28 +00:00
}
2019-05-09 06:54:39 +00:00
2025-05-26 06:08:22 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2021-05-09 21:20:57 +00:00
2020-06-30 11:09:28 +00:00
if ( $membership -> getAttribute ( 'confirm' )) { // Count only confirmed members
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> decreaseDocumentAttribute ( 'teams' , $team -> getId (), 'total' , 1 , 0 ));
2019-05-09 06:54:39 +00:00
}
2020-06-30 11:09:28 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-04-13 12:39:31 +00:00
-> setParam ( 'teamId' , $team -> getId ())
2025-05-26 05:59:46 +00:00
-> setParam ( 'userId' , $profile -> getId ())
2022-04-13 12:39:31 +00:00
-> setParam ( 'membershipId' , $membership -> getId ())
-> setPayload ( $response -> output ( $membership , Response :: MODEL_MEMBERSHIP ))
2020-12-02 22:15:20 +00:00
;
2020-06-30 11:09:28 +00:00
$response -> noContent ();
2020-12-26 16:48:43 +00:00
});
2022-04-21 14:07:08 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/teams/:teamId/logs' )
2023-08-01 15:26:48 +00:00
-> desc ( 'List team logs' )
2022-04-21 14:07:08 +00:00
-> groups ([ 'api' , 'teams' ])
-> label ( 'scope' , 'teams.read' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'teams' ,
2025-03-31 05:48:17 +00:00
group : 'logs' ,
2025-01-17 04:31:39 +00:00
name : 'listLogs' ,
description : '/docs/references/teams/get-team-logs.md' ,
auth : [ AuthType :: ADMIN ],
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_LOG_LIST ,
)
]
))
2022-09-19 10:05:42 +00:00
-> param ( 'teamId' , '' , new UID (), 'Team ID.' )
2023-05-16 12:56:20 +00:00
-> param ( 'queries' , [], new Queries ([ new Limit (), new Offset ()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset' , true )
2025-11-05 02:28:56 +00:00
-> param ( 'total' , true , new Boolean ( true ), 'When set to false, the total count returned will be 0 and will not be calculated.' , true )
2022-04-21 14:07:08 +00:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'locale' )
-> inject ( 'geodb' )
2025-12-14 01:43:35 +00:00
-> inject ( 'audit' )
-> action ( function ( string $teamId , array $queries , bool $includeTotal , Response $response , Database $dbForProject , Locale $locale , Reader $geodb , Audit $audit ) {
2022-04-21 14:07:08 +00:00
$team = $dbForProject -> getDocument ( 'teams' , $teamId );
if ( $team -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: TEAM_NOT_FOUND );
2022-04-21 14:07:08 +00:00
}
2024-02-12 16:02:04 +00:00
try {
$queries = Query :: parseQueries ( $queries );
} catch ( QueryException $e ) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $e -> getMessage ());
}
2025-12-15 02:50:21 +00:00
$grouped = Query :: groupByType ( $queries );
$limit = $grouped [ 'limit' ] ? ? 25 ;
$offset = $grouped [ 'offset' ] ? ? 0 ;
2022-05-23 14:54:50 +00:00
$resource = 'team/' . $team -> getId ();
2025-12-15 02:50:21 +00:00
$logs = $audit -> getLogsByResource ( $resource , offset : $offset , limit : $limit );
2022-04-21 14:07:08 +00:00
$output = [];
foreach ( $logs as $i => & $log ) {
$log [ 'userAgent' ] = ( ! empty ( $log [ 'userAgent' ])) ? $log [ 'userAgent' ] : 'UNKNOWN' ;
$detector = new Detector ( $log [ 'userAgent' ]);
$detector -> skipBotDetection (); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector -> getOS ();
$client = $detector -> getClient ();
$device = $detector -> getDevice ();
$output [ $i ] = new Document ([
'event' => $log [ 'event' ],
2023-07-12 17:27:57 +00:00
'userId' => $log [ 'data' ][ 'userId' ],
2022-04-21 14:07:08 +00:00
'userEmail' => $log [ 'data' ][ 'userEmail' ] ? ? null ,
'userName' => $log [ 'data' ][ 'userName' ] ? ? null ,
'mode' => $log [ 'data' ][ 'mode' ] ? ? null ,
'ip' => $log [ 'ip' ],
'time' => $log [ 'time' ],
'osCode' => $os [ 'osCode' ],
'osName' => $os [ 'osName' ],
'osVersion' => $os [ 'osVersion' ],
'clientType' => $client [ 'clientType' ],
'clientCode' => $client [ 'clientCode' ],
'clientName' => $client [ 'clientName' ],
'clientVersion' => $client [ 'clientVersion' ],
'clientEngine' => $client [ 'clientEngine' ],
'clientEngineVersion' => $client [ 'clientEngineVersion' ],
'deviceName' => $device [ 'deviceName' ],
'deviceBrand' => $device [ 'deviceBrand' ],
'deviceModel' => $device [ 'deviceModel' ]
]);
$record = $geodb -> get ( $log [ 'ip' ]);
if ( $record ) {
2022-05-23 14:54:50 +00:00
$output [ $i ][ 'countryCode' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), false ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), $locale -> getText ( 'locale.country.unknown' ));
2022-04-21 14:07:08 +00:00
} else {
$output [ $i ][ 'countryCode' ] = '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'locale.country.unknown' );
}
}
$response -> dynamic ( new Document ([
2025-12-15 02:50:21 +00:00
'total' => $includeTotal ? $audit -> countLogsByResource ( $resource ) : 0 ,
2025-02-25 06:21:35 +00:00
'logs' => $output ,
2022-04-21 14:07:08 +00:00
]), Response :: MODEL_LOG_LIST );
2025-01-17 04:39:16 +00:00
});