mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '0.7.x' of github.com:appwrite/appwrite into 0.7.x
This commit is contained in:
@@ -300,6 +300,15 @@ $collections = [
|
||||
'name' => 'Token',
|
||||
'structure' => true,
|
||||
'rules' => [
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'User ID',
|
||||
'key' => 'userId',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Type',
|
||||
|
||||
+32
-2
@@ -19,6 +19,18 @@ return [
|
||||
'account.update.prefs' => [
|
||||
'description' => 'This event triggers when the account preferences are updated.',
|
||||
],
|
||||
'account.recovery.create' => [
|
||||
'description' => 'This event triggers when the account recovery token is created.',
|
||||
],
|
||||
'account.recovery.update' => [
|
||||
'description' => 'This event triggers when the account recovery token is validated.',
|
||||
],
|
||||
'account.verification.create' => [
|
||||
'description' => 'This event triggers when the account verification token is created.',
|
||||
],
|
||||
'account.verification.update' => [
|
||||
'description' => 'This event triggers when the account verification token is validated.',
|
||||
],
|
||||
'account.delete' => [
|
||||
'description' => 'This event triggers when the account is deleted.',
|
||||
],
|
||||
@@ -40,8 +52,8 @@ return [
|
||||
'database.documents.create' => [
|
||||
'description' => 'This event triggers when a database document is created.',
|
||||
],
|
||||
'database.documents.patch' => [
|
||||
'description' => 'This event triggers when a database document is patched.',
|
||||
'database.documents.update' => [
|
||||
'description' => 'This event triggers when a database document is updated.',
|
||||
],
|
||||
'database.documents.delete' => [
|
||||
'description' => 'This event triggers when a database document is deleted.',
|
||||
@@ -67,4 +79,22 @@ return [
|
||||
'users.sessions.delete' => [
|
||||
'description' => 'This event triggers when a user session is deleted from users API.',
|
||||
],
|
||||
'teams.create' => [
|
||||
'description' => 'This event triggers when a team is created.',
|
||||
],
|
||||
'teams.update' => [
|
||||
'description' => 'This event triggers when a team is updated.',
|
||||
],
|
||||
'teams.delete' => [
|
||||
'description' => 'This event triggers when a team is deleted.',
|
||||
],
|
||||
'teams.memberships.create' => [
|
||||
'description' => 'This event triggers when a team memberships is created.',
|
||||
],
|
||||
'teams.memberships.update.status' => [
|
||||
'description' => 'This event triggers when a team memberships status is updated.',
|
||||
],
|
||||
'teams.memberships.delete' => [
|
||||
'description' => 'This event triggers when a team memberships is deleted.',
|
||||
],
|
||||
];
|
||||
@@ -105,6 +105,10 @@ App::post('/v1/account')
|
||||
|
||||
Authorization::enable();
|
||||
|
||||
Authorization::unsetRole('role:'.Auth::USER_ROLE_GUEST);
|
||||
Authorization::setRole('user:'.$user->getId());
|
||||
Authorization::setRole('role:'.Auth::USER_ROLE_MEMBER);
|
||||
|
||||
if (false === $user) {
|
||||
throw new Exception('Failed saving user to DB', 500);
|
||||
}
|
||||
@@ -115,10 +119,6 @@ App::post('/v1/account')
|
||||
->setParam('resource', 'users/'.$user->getId())
|
||||
;
|
||||
|
||||
$user
|
||||
->setAttribute('roles', Authorization::getRoles())
|
||||
;
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER)
|
||||
@@ -190,12 +190,12 @@ App::post('/v1/account/sessions')
|
||||
$session = new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]],
|
||||
'userId' => $profile->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
|
||||
'osCode' => $osCode,
|
||||
'osName' => $osName,
|
||||
'osVersion' => $osVersion,
|
||||
@@ -505,7 +505,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
|
||||
// Create session token, verify user account and update OAuth2 ID and Access Token
|
||||
|
||||
|
||||
$dd = new DeviceDetector($request->getUserAgent('UNKNOWN'));
|
||||
|
||||
$dd->parse();
|
||||
@@ -528,12 +527,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$session = new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
|
||||
'osCode' => $osCode,
|
||||
'osName' => $osName,
|
||||
'osVersion' => $osVersion,
|
||||
@@ -624,10 +623,6 @@ App::get('/v1/account')
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Document $user */
|
||||
|
||||
$user
|
||||
->setAttribute('roles', Authorization::getRoles())
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
}, ['response', 'user']);
|
||||
|
||||
@@ -818,8 +813,6 @@ App::patch('/v1/account/name')
|
||||
throw new Exception('Failed saving user to DB', 500);
|
||||
}
|
||||
|
||||
$user->setAttribute('roles', Authorization::getRoles());
|
||||
|
||||
$audits
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('event', 'account.update.name')
|
||||
@@ -861,8 +854,6 @@ App::patch('/v1/account/password')
|
||||
throw new Exception('Failed saving user to DB', 500);
|
||||
}
|
||||
|
||||
$user->setAttribute('roles', Authorization::getRoles());
|
||||
|
||||
$audits
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('event', 'account.update.password')
|
||||
@@ -918,8 +909,6 @@ App::patch('/v1/account/email')
|
||||
if (false === $user) {
|
||||
throw new Exception('Failed saving user to DB', 500);
|
||||
}
|
||||
|
||||
$user->setAttribute('roles', Authorization::getRoles());
|
||||
|
||||
$audits
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -962,9 +951,7 @@ App::patch('/v1/account/prefs')
|
||||
->setParam('resource', 'users/'.$user->getId())
|
||||
;
|
||||
|
||||
$prefs = $user->getAttribute('prefs', new \stdClass);
|
||||
|
||||
$response->dynamic(new Document($prefs), Response::MODEL_ANY);
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
}, ['response', 'user', 'projectDB', 'audits']);
|
||||
|
||||
App::delete('/v1/account')
|
||||
@@ -1069,23 +1056,27 @@ App::delete('/v1/account/sessions/:sessionId')
|
||||
->setParam('resource', '/user/'.$user->getId())
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($user, Response::MODEL_USER))
|
||||
;
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response
|
||||
->addHeader('X-Fallback-Cookies', \json_encode([]))
|
||||
;
|
||||
}
|
||||
|
||||
$token->setAttribute('current', false);
|
||||
|
||||
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$token->setAttribute('current', true);
|
||||
|
||||
$response
|
||||
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
;
|
||||
}
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($token, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
return $response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -1127,10 +1118,6 @@ App::delete('/v1/account/sessions')
|
||||
->setParam('event', 'account.sessions.delete')
|
||||
->setParam('resource', '/user/'.$user->getId())
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($user, Response::MODEL_USER))
|
||||
;
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response
|
||||
@@ -1138,13 +1125,23 @@ App::delete('/v1/account/sessions')
|
||||
;
|
||||
}
|
||||
|
||||
$token->setAttribute('current', false);
|
||||
|
||||
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$token->setAttribute('current', true);
|
||||
$response
|
||||
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output(new Document([
|
||||
'sum' => count($tokens),
|
||||
'sessions' => $tokens
|
||||
]), Response::MODEL_SESSION_LIST))
|
||||
;
|
||||
|
||||
$response->noContent();
|
||||
}, ['request', 'response', 'user', 'projectDB', 'audits', 'webhooks']);
|
||||
@@ -1153,6 +1150,7 @@ App::post('/v1/account/recovery')
|
||||
->desc('Create Password Recovery')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('event', 'account.recovery.create')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createRecovery')
|
||||
@@ -1164,7 +1162,7 @@ App::post('/v1/account/recovery')
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. 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.', false, ['clients'])
|
||||
->action(function ($email, $url, $request, $response, $projectDB, $project, $locale, $mails, $audits) {
|
||||
->action(function ($email, $url, $request, $response, $projectDB, $project, $locale, $mails, $audits, $webhooks) {
|
||||
/** @var Utopia\Swoole\Request $request */
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
@@ -1172,6 +1170,10 @@ App::post('/v1/account/recovery')
|
||||
/** @var Utopia\Locale\Locale $locale */
|
||||
/** @var Appwrite\Event\Event $mails */
|
||||
/** @var Appwrite\Event\Event $audits */
|
||||
/** @var Appwrite\Event\Event $webhooks */
|
||||
|
||||
$isPreviliggedUser = Auth::isPreviliggedUser(Authorization::$roles);
|
||||
$isAppUser = Auth::isAppUser(Authorization::$roles);
|
||||
|
||||
$profile = $projectDB->getCollectionFirst([ // Get user by email address
|
||||
'limit' => 1,
|
||||
@@ -1189,6 +1191,7 @@ App::post('/v1/account/recovery')
|
||||
$recovery = new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]],
|
||||
'userId' => $profile->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY,
|
||||
@@ -1246,6 +1249,17 @@ App::post('/v1/account/recovery')
|
||||
->trigger();
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload',
|
||||
$response->output($recovery->setAttribute('secret', $secret),
|
||||
Response::MODEL_TOKEN
|
||||
))
|
||||
;
|
||||
|
||||
$recovery // Hide secret for clients, sp
|
||||
->setAttribute('secret',
|
||||
($isPreviliggedUser || $isAppUser) ? $secret : '');
|
||||
|
||||
$audits
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('event', 'account.recovery.create')
|
||||
@@ -1256,12 +1270,13 @@ App::post('/v1/account/recovery')
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($recovery, Response::MODEL_TOKEN)
|
||||
;
|
||||
}, ['request', 'response', 'projectDB', 'project', 'locale', 'mails', 'audits']);
|
||||
}, ['request', 'response', 'projectDB', 'project', 'locale', 'mails', 'audits', 'webhooks']);
|
||||
|
||||
App::put('/v1/account/recovery')
|
||||
->desc('Complete Password Recovery')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('event', 'account.recovery.update')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateRecovery')
|
||||
@@ -1337,6 +1352,7 @@ App::post('/v1/account/verification')
|
||||
->desc('Create Email Verification')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('event', 'account.verification.create')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createVerification')
|
||||
@@ -1347,7 +1363,7 @@ App::post('/v1/account/verification')
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. 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.', false, ['clients']) // TODO add built-in confirm page
|
||||
->action(function ($url, $request, $response, $project, $user, $projectDB, $locale, $audits, $mails) {
|
||||
->action(function ($url, $request, $response, $project, $user, $projectDB, $locale, $audits, $webhooks, $mails) {
|
||||
/** @var Utopia\Swoole\Request $request */
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Document $project */
|
||||
@@ -1355,13 +1371,18 @@ App::post('/v1/account/verification')
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
/** @var Utopia\Locale\Locale $locale */
|
||||
/** @var Appwrite\Event\Event $audits */
|
||||
/** @var Appwrite\Event\Event $webhooks */
|
||||
/** @var Appwrite\Event\Event $mails */
|
||||
|
||||
$isPreviliggedUser = Auth::isPreviliggedUser(Authorization::$roles);
|
||||
$isAppUser = Auth::isAppUser(Authorization::$roles);
|
||||
|
||||
$verificationSecret = Auth::tokenGenerator();
|
||||
|
||||
$verification = new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_VERIFICATION,
|
||||
'secret' => Auth::hash($verificationSecret), // One way hash encryption to protect DB leak
|
||||
'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM,
|
||||
@@ -1419,6 +1440,17 @@ App::post('/v1/account/verification')
|
||||
->trigger()
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload',
|
||||
$response->output($verification->setAttribute('secret', $verificationSecret),
|
||||
Response::MODEL_TOKEN
|
||||
))
|
||||
;
|
||||
|
||||
$verification // Hide secret for clients, sp
|
||||
->setAttribute('secret',
|
||||
($isPreviliggedUser || $isAppUser) ? $verificationSecret : '');
|
||||
|
||||
$audits
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('event', 'account.verification.create')
|
||||
@@ -1429,12 +1461,13 @@ App::post('/v1/account/verification')
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($verification, Response::MODEL_TOKEN)
|
||||
;
|
||||
}, ['request', 'response', 'project', 'user', 'projectDB', 'locale', 'audits', 'mails']);
|
||||
}, ['request', 'response', 'project', 'user', 'projectDB', 'locale', 'audits', 'webhooks', 'mails']);
|
||||
|
||||
App::put('/v1/account/verification')
|
||||
->desc('Complete Email Verification')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('event', 'account.verification.update')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateVerification')
|
||||
|
||||
@@ -235,7 +235,7 @@ App::delete('/v1/database/collections/:collectionId')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('collectionId', '', new UID(), 'Collection unique ID.')
|
||||
->action(function ($collectionId, $response, $projectDB, $webhooks, $audits) {
|
||||
->action(function ($collectionId, $response, $projectDB, $webhooks, $audits, $deletes) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
/** @var Appwrite\Event\Event $webhooks */
|
||||
@@ -250,7 +250,11 @@ App::delete('/v1/database/collections/:collectionId')
|
||||
if (!$projectDB->deleteDocument($collectionId)) {
|
||||
throw new Exception('Failed to remove collection from DB', 500);
|
||||
}
|
||||
|
||||
|
||||
$deletes
|
||||
->setParam('document', $collection)
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION))
|
||||
;
|
||||
@@ -262,7 +266,7 @@ App::delete('/v1/database/collections/:collectionId')
|
||||
;
|
||||
|
||||
$response->noContent();
|
||||
}, ['response', 'projectDB', 'webhooks', 'audits']);
|
||||
}, ['response', 'projectDB', 'webhooks', 'audits', 'deletes']);
|
||||
|
||||
App::post('/v1/database/collections/:collectionId/documents')
|
||||
->desc('Create Document')
|
||||
|
||||
@@ -23,6 +23,7 @@ use DeviceDetector\DeviceDetector;
|
||||
App::post('/v1/teams')
|
||||
->desc('Create Team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.create')
|
||||
->label('scope', 'teams.write')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -157,6 +158,7 @@ App::get('/v1/teams/:teamId')
|
||||
App::put('/v1/teams/:teamId')
|
||||
->desc('Update Team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.update')
|
||||
->label('scope', 'teams.write')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -191,6 +193,7 @@ App::put('/v1/teams/:teamId')
|
||||
App::delete('/v1/teams/:teamId')
|
||||
->desc('Delete Team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.delete')
|
||||
->label('scope', 'teams.write')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -200,9 +203,10 @@ App::delete('/v1/teams/:teamId')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('teamId', '', new UID(), 'Team unique ID.')
|
||||
->action(function ($teamId, $response, $projectDB) {
|
||||
->action(function ($teamId, $response, $projectDB, $webhooks) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
/** @var Appwrite\Event\Event $webhooks */
|
||||
|
||||
$team = $projectDB->getDocument($teamId);
|
||||
|
||||
@@ -229,12 +233,17 @@ App::delete('/v1/teams/:teamId')
|
||||
throw new Exception('Failed to remove team from DB', 500);
|
||||
}
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($team, Response::MODEL_TEAM))
|
||||
;
|
||||
|
||||
$response->noContent();
|
||||
}, ['response', 'projectDB']);
|
||||
}, ['response', 'projectDB', 'webhooks']);
|
||||
|
||||
App::post('/v1/teams/:teamId/memberships')
|
||||
->desc('Create Team Membership')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.memberships.create')
|
||||
->label('scope', 'teams.write')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -483,6 +492,7 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
|
||||
->desc('Update Team Membership Status')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.memberships.update.status')
|
||||
->label('scope', 'public')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -581,6 +591,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
|
||||
$session = new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
@@ -661,6 +672,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
|
||||
App::delete('/v1/teams/:teamId/memberships/:inviteId')
|
||||
->desc('Delete Team Membership')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.memberships.delete')
|
||||
->label('scope', 'teams.write')
|
||||
->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER])
|
||||
->label('sdk.namespace', 'teams')
|
||||
@@ -671,10 +683,11 @@ App::delete('/v1/teams/:teamId/memberships/:inviteId')
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('teamId', '', new UID(), 'Team unique ID.')
|
||||
->param('inviteId', '', new UID(), 'Invite unique ID.')
|
||||
->action(function ($teamId, $inviteId, $response, $projectDB, $audits) {
|
||||
->action(function ($teamId, $inviteId, $response, $projectDB, $audits, $webhooks) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
/** @var Appwrite\Event\Event $audits */
|
||||
/** @var Appwrite\Event\Event $webhooks */
|
||||
|
||||
$membership = $projectDB->getDocument($inviteId);
|
||||
|
||||
@@ -712,5 +725,9 @@ App::delete('/v1/teams/:teamId/memberships/:inviteId')
|
||||
->setParam('resource', 'teams/'.$teamId)
|
||||
;
|
||||
|
||||
$webhooks
|
||||
->setParam('payload', $response->output($membership, Response::MODEL_MEMBERSHIP))
|
||||
;
|
||||
|
||||
$response->noContent();
|
||||
}, ['response', 'projectDB', 'audits']);
|
||||
}, ['response', 'projectDB', 'audits', 'webhooks']);
|
||||
|
||||
@@ -170,7 +170,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||
*/
|
||||
if (null !== $key && $user->isEmpty()) {
|
||||
$user = new Document([
|
||||
'$id' => 0,
|
||||
'$id' => '',
|
||||
'status' => Auth::USER_STATUS_ACTIVATED,
|
||||
'email' => 'app.'.$project->getId().'@service.'.$request->getHostname(),
|
||||
'password' => '',
|
||||
@@ -222,6 +222,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
|
||||
*/
|
||||
$webhooks
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('event', $route->getLabel('event', ''))
|
||||
->setParam('payload', [])
|
||||
;
|
||||
@@ -281,8 +282,8 @@ App::shutdown(function ($utopia, $request, $response, $project, $webhooks, $audi
|
||||
$route = $utopia->match($request);
|
||||
|
||||
if ($project->getId()
|
||||
&& $mode !== APP_MODE_ADMIN
|
||||
&& !empty($route->getLabel('sdk.namespace', null))) { // Don't calculate console usage and admin mode
|
||||
&& $mode !== APP_MODE_ADMIN //TODO: add check to make sure user is admin
|
||||
&& !empty($route->getLabel('sdk.namespace', null))) { // Don't calculate console usage on admin mode
|
||||
|
||||
$usage
|
||||
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
|
||||
|
||||
+153
-28
@@ -10,31 +10,43 @@ use Appwrite\Database\Database;
|
||||
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
|
||||
use Appwrite\Database\Adapter\Redis as RedisAdapter;
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Database\Validator\Authorization;
|
||||
use Appwrite\Storage\Device\Local;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
|
||||
class DeletesV1
|
||||
{
|
||||
public $args = [];
|
||||
|
||||
protected $consoleDB = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function perform()
|
||||
{
|
||||
$projectId = $this->args['projectId'];
|
||||
$document = $this->args['document'];
|
||||
|
||||
$document = new Document($document);
|
||||
|
||||
switch ($document->getCollection()) {
|
||||
switch (strval($document->getCollection())) {
|
||||
case Database::SYSTEM_COLLECTION_PROJECTS:
|
||||
$this->deleteProject($document);
|
||||
break;
|
||||
case Database::SYSTEM_COLLECTION_FUNCTIONS:
|
||||
$this->deleteFunction($document, $projectId);
|
||||
break;
|
||||
case Database::SYSTEM_COLLECTION_USERS:
|
||||
$this->deleteUser($document);
|
||||
$this->deleteUser($document, $projectId);
|
||||
break;
|
||||
case Database::SYSTEM_COLLECTION_COLLECTIONS:
|
||||
$this->deleteDocuments($document, $projectId);
|
||||
break;
|
||||
|
||||
default:
|
||||
Console::error('No lazy delete operation available for document of type: '.$document->getCollection());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -43,50 +55,163 @@ class DeletesV1
|
||||
{
|
||||
// ... Remove environment for this job
|
||||
}
|
||||
|
||||
protected function deleteDocuments(Document $document, $projectId)
|
||||
{
|
||||
$collectionId = $document->getId();
|
||||
|
||||
// Delete Documents in the deleted collection
|
||||
$this->deleteByGroup([
|
||||
'$collection='.$collectionId
|
||||
], $this->getProjectDB($projectId));
|
||||
}
|
||||
|
||||
protected function deleteProject(Document $document)
|
||||
{
|
||||
global $register;
|
||||
|
||||
$consoleDB = new Database();
|
||||
$consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register));
|
||||
$consoleDB->setNamespace('app_console'); // Main DB
|
||||
$consoleDB->setMocks(Config::getParam('collections', []));
|
||||
|
||||
// Delete all DBs
|
||||
$consoleDB->deleteNamespace($document->getId());
|
||||
$this->getConsoleDB()->deleteNamespace($document->getId());
|
||||
$uploads = new Local(APP_STORAGE_UPLOADS.'/app-'.$document->getId());
|
||||
$cache = new Local(APP_STORAGE_CACHE.'/app-'.$document->getId());
|
||||
|
||||
// Delete all storage directories
|
||||
$uploads->delete($uploads->getRoot(), true);
|
||||
$cache->delete($cache->getRoot(), true);
|
||||
}
|
||||
|
||||
protected function deleteUser(Document $user)
|
||||
protected function deleteUser(Document $document, $projectId)
|
||||
{
|
||||
global $projectDB;
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$tokens = $document->getAttribute('tokens', []);
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (!$projectDB->deleteDocument($token->getId())) {
|
||||
if (!$this->getProjectDB($projectId)->deleteDocument($token->getId())) {
|
||||
throw new Exception('Failed to remove token from DB', 500);
|
||||
}
|
||||
}
|
||||
|
||||
$memberships = $projectDB->getCollection([
|
||||
'limit' => 2000, // TODO add members limit
|
||||
'offset' => 0,
|
||||
'filters' => [
|
||||
'$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS,
|
||||
'userId='.$user->getId(),
|
||||
],
|
||||
]);
|
||||
// Delete Memberships
|
||||
$this->deleteByGroup([
|
||||
'$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS,
|
||||
'userId='.$document->getId(),
|
||||
], $this->getProjectDB($projectId));
|
||||
}
|
||||
|
||||
foreach ($memberships as $membership) {
|
||||
if (!$projectDB->deleteDocument($membership->getId())) {
|
||||
throw new Exception('Failed to remove team membership from DB', 500);
|
||||
protected function deleteFunction(Document $document, $projectId)
|
||||
{
|
||||
$projectDB = $this->getProjectDB($projectId);
|
||||
$device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId);
|
||||
|
||||
// Delete Tags
|
||||
$this->deleteByGroup([
|
||||
'$collection='.Database::SYSTEM_COLLECTION_TAGS,
|
||||
'functionId='.$document->getId(),
|
||||
], $projectDB, function(Document $document) use ($device) {
|
||||
|
||||
if ($device->delete($document->getAttribute('path', ''))) {
|
||||
Console::success('Delete code tag: '.$document->getAttribute('path', ''));
|
||||
}
|
||||
else {
|
||||
Console::error('Dailed to delete code tag: '.$document->getAttribute('path', ''));
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Executions
|
||||
$this->deleteByGroup([
|
||||
'$collection='.Database::SYSTEM_COLLECTION_EXECUTIONS,
|
||||
'functionId='.$document->getId(),
|
||||
], $projectDB);
|
||||
}
|
||||
|
||||
protected function deleteById(Document $document, Database $database, callable $callback = null): bool
|
||||
{
|
||||
Authorization::disable();
|
||||
|
||||
if($database->deleteDocument($document->getId())) {
|
||||
Console::success('Deleted document "'.$document->getId().'" successfully');
|
||||
|
||||
if(is_callable($callback)) {
|
||||
$callback($document);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Console::error('Failed to delete document: '.$document->getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
Authorization::reset();
|
||||
}
|
||||
|
||||
protected function deleteByGroup(array $filters, Database $database, callable $callback = null)
|
||||
{
|
||||
$count = 0;
|
||||
$chunk = 0;
|
||||
$limit = 50;
|
||||
$results = [];
|
||||
$sum = $limit;
|
||||
|
||||
$executionStart = \microtime(true);
|
||||
|
||||
while($sum === $limit) {
|
||||
$chunk++;
|
||||
|
||||
Authorization::disable();
|
||||
|
||||
$results = $database->getCollection([
|
||||
'limit' => $limit,
|
||||
'offset' => 0,
|
||||
'orderField' => '$id',
|
||||
'orderType' => 'ASC',
|
||||
'orderCast' => 'string',
|
||||
'filters' => $filters,
|
||||
]);
|
||||
|
||||
Authorization::reset();
|
||||
|
||||
$sum = count($results);
|
||||
|
||||
Console::info('Deleting chunk #'.$chunk.'. Found '.$sum.' documents');
|
||||
|
||||
foreach ($results as $document) {
|
||||
$this->deleteById($document, $database, $callback);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$executionEnd = \microtime(true);
|
||||
|
||||
Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Database;
|
||||
*/
|
||||
protected function getConsoleDB(): Database
|
||||
{
|
||||
global $register;
|
||||
|
||||
if($this->consoleDB === null) {
|
||||
$this->consoleDB = new Database();
|
||||
$this->consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register));
|
||||
$this->consoleDB->setNamespace('app_console'); // Main DB
|
||||
$this->consoleDB->setMocks(Config::getParam('collections', []));
|
||||
}
|
||||
|
||||
return $this->consoleDB;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Database;
|
||||
*/
|
||||
protected function getProjectDB($projectId): Database
|
||||
{
|
||||
global $register;
|
||||
|
||||
$projectDB = new Database();
|
||||
$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register));
|
||||
$projectDB->setNamespace('app_'.$projectId); // Main DB
|
||||
$projectDB->setMocks(Config::getParam('collections', []));
|
||||
|
||||
return $projectDB;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
|
||||
require_once __DIR__.'/../init.php';
|
||||
|
||||
@@ -23,6 +24,11 @@ class MailsV1
|
||||
{
|
||||
global $register;
|
||||
|
||||
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
Console::info('Skipped mail processing. No SMTP server hostname has been set.');
|
||||
return;
|
||||
}
|
||||
|
||||
$event = $this->args['event'];
|
||||
$from = $this->args['from'];
|
||||
$recipient = $this->args['recipient'];
|
||||
|
||||
@@ -34,8 +34,9 @@ class WebhooksV1
|
||||
$errors = [];
|
||||
|
||||
// Event
|
||||
$projectId = $this->args['projectId'];
|
||||
$event = $this->args['event'];
|
||||
$projectId = $this->args['projectId'] ?? '';
|
||||
$userId = $this->args['userId'] ?? '';
|
||||
$event = $this->args['event'] ?? '';
|
||||
$payload = \json_encode($this->args['payload']);
|
||||
|
||||
// Webhook
|
||||
@@ -55,6 +56,7 @@ class WebhooksV1
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $webhook['$id'] ?? '';
|
||||
$name = $webhook['name'] ?? '';
|
||||
$signature = $webhook['signature'] ?? 'not-yet-implemented';
|
||||
$url = $webhook['url'] ?? '';
|
||||
@@ -78,8 +80,11 @@ class WebhooksV1
|
||||
[
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: '.\strlen($payload),
|
||||
'X-'.APP_NAME.'-Webhook-Id: '.$id,
|
||||
'X-'.APP_NAME.'-Webhook-Event: '.$event,
|
||||
'X-'.APP_NAME.'-Webhook-Name: '.$name,
|
||||
'X-'.APP_NAME.'-Webhook-User-Id: '.$userId,
|
||||
'X-'.APP_NAME.'-Webhook-Project-Id: '.$projectId,
|
||||
'X-'.APP_NAME.'-Webhook-Signature: '.$signature,
|
||||
]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user