1
0
Fork 0
mirror of synced 2024-06-02 10:54:44 +12:00

sync with devices

This commit is contained in:
Torsten Dittmann 2022-04-04 08:30:07 +02:00
parent 6de577191b
commit 2f9b9445dd
15 changed files with 785 additions and 337 deletions

View file

@ -282,4 +282,10 @@ return [
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.7',
],
'users.*' => [
'description' => 'This event triggers when a team memberships is deleted.',
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.7',
],
];

View file

@ -23,7 +23,6 @@ use Utopia\Database\Validator\UID;
use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -34,7 +33,7 @@ $oauthDefaultFailure = '/v1/auth/oauth2/failure';
App::post('/v1/account')
->desc('Create Account')
->groups(['api', 'account', 'auth'])
->label('event', 'account.create')
->label('event', 'users.[userId].create')
->label('scope', 'public')
->label('auth.type', 'emailPassword')
->label('sdk.auth', [])
@ -55,13 +54,15 @@ App::post('/v1/account')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($userId, $email, $password, $name, $request, $response, $project, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($userId, $email, $password, $name, $request, $response, $project, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$email = \strtolower($email);
if ('console' === $project->getId()) {
@ -120,13 +121,19 @@ App::post('/v1/account')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.create')
->setParam('resource', 'user/' . $user->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
->setParam('users.create', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
@ -134,7 +141,7 @@ App::post('/v1/account')
App::post('/v1/account/sessions')
->desc('Create Account Session')
->groups(['api', 'account', 'auth'])
->label('event', 'account.sessions.create')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('auth.type', 'emailPassword')
->label('sdk.auth', [])
@ -155,7 +162,8 @@ App::post('/v1/account/sessions')
->inject('geodb')
->inject('audits')
->inject('usage')
->action(function ($email, $password, $request, $response, $dbForProject, $locale, $geodb, $audits, $usage) {
->inject('events')
->action(function ($email, $password, $request, $response, $dbForProject, $locale, $geodb, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
@ -163,6 +171,7 @@ App::post('/v1/account/sessions')
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$email = \strtolower($email);
$protocol = $request->getProtocol();
@ -170,12 +179,6 @@ App::post('/v1/account/sessions')
$profile = $dbForProject->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) {
$audits
//->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.failed')
->setParam('resource', 'user/'.($profile ? $profile->getId() : ''))
;
throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS); // Wrong password or username
}
@ -213,10 +216,11 @@ App::post('/v1/account/sessions')
$audits
->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'user/' . $profile->getId())
->setParam('userEmail', $profile->getAttribute('email', ''))
->setParam('userName', $profile->getAttribute('name', ''))
->setParam('sessionId', $session->getId())
->setUser($profile)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$profile->getId()
]))
;
if (!Config::getParam('domainVerification')) {
@ -243,6 +247,12 @@ App::post('/v1/account/sessions')
->setParam('users.sessions.create', 1)
->setParam('provider', 'email')
;
$events
->setParam('userId', $profile->getId())
->setParam('sessionId', $session->getId())
;
$response->dynamic($session, Response::MODEL_SESSION);
});
@ -365,7 +375,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->desc('OAuth2 Redirect')
->groups(['api', 'account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('event', 'account.sessions.create')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
@ -390,6 +400,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
/** @var Utopia\Database\Database $dbForProject */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$protocol = $request->getProtocol();
@ -567,18 +578,25 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'user/' . $user->getId())
->setParam('data', ['provider' => $provider])
->setParam('sessionId', $session->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$events->setParam('eventData', $response->output($session, Response::MODEL_SESSION));
$usage
->setParam('users.sessions.create', 1)
->setParam('projectId', $project->getId())
->setParam('provider', 'oauth2-'.$provider)
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
@ -687,7 +705,6 @@ App::post('/v1/account/sessions/magic-url')
])));
$mails->setParam('event', 'users.create');
$audits->setParam('event', 'users.create');
}
$loginSecret = Auth::tokenGenerator();
@ -723,12 +740,14 @@ App::post('/v1/account/sessions/magic-url')
$url = Template::unParseURL($url);
$mails
->setParam('from', $project->getId())
->setParam('recipient', $user->getAttribute('email'))
->setParam('url', $url)
->setParam('locale', $locale->default)
->setParam('project', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('type', MAIL_TYPE_MAGIC_SESSION)
->setPayload([
'from' => $project->getId(),
'recipient' => $user->getAttribute('email'),
'url' => $url,
'locale' => $locale->default,
'project' => $project->getAttribute('name', ['[APP-NAME]']),
'type' => MAIL_TYPE_MAGIC_SESSION
])
->trigger()
;
@ -744,8 +763,10 @@ App::post('/v1/account/sessions/magic-url')
($isPrivilegedUser || $isAppUser) ? $loginSecret : '');
$audits
->setParam('userId', $user->getId())
->setParam('resource', 'users/'.$user->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$response
@ -758,7 +779,7 @@ App::put('/v1/account/sessions/magic-url')
->desc('Create Magic URL session (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.sessions.create')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateMagicURLSession')
@ -776,7 +797,8 @@ App::put('/v1/account/sessions/magic-url')
->inject('locale')
->inject('geodb')
->inject('audits')
->action(function ($userId, $secret, $request, $response, $dbForProject, $locale, $geodb, $audits) {
->inject('events')
->action(function ($userId, $secret, $request, $response, $dbForProject, $locale, $geodb, $audits, $events) {
/** @var string $userId */
/** @var string $secret */
/** @var Appwrite\Utopia\Request $request */
@ -785,6 +807,7 @@ App::put('/v1/account/sessions/magic-url')
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -850,8 +873,15 @@ App::put('/v1/account/sessions/magic-url')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'users/'.$user->getId())
->setParam('sessionId', $session->getId())
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
if (!Config::getParam('domainVerification')) {
@ -883,7 +913,7 @@ App::put('/v1/account/sessions/magic-url')
App::post('/v1/account/sessions/anonymous')
->desc('Create Anonymous Session')
->groups(['api', 'account', 'auth'])
->label('event', 'account.sessions.create')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('auth.type', 'anonymous')
->label('sdk.auth', [])
@ -904,7 +934,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('geodb')
->inject('audits')
->inject('usage')
->action(function ($request, $response, $locale, $user, $project, $dbForProject, $geodb, $audits, $usage) {
->inject('events')
->action(function ($request, $response, $locale, $user, $project, $dbForProject, $geodb, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Locale\Locale $locale */
@ -914,6 +945,7 @@ App::post('/v1/account/sessions/anonymous')
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Stats\Stats $events */
$protocol = $request->getProtocol();
@ -992,8 +1024,10 @@ App::post('/v1/account/sessions/anonymous')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'user/' . $user->getId())
->setParam('sessionId', $session->getId())
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
@ -1001,6 +1035,11 @@ App::post('/v1/account/sessions/anonymous')
->setParam('provider', 'anonymous')
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
@ -1196,26 +1235,8 @@ App::get('/v1/account/logs')
/** @var Appwrite\Stats\Stats $usage */
$audit = new Audit($dbForProject);
$auditEvents = [
'account.create',
'account.delete',
'account.update.name',
'account.update.email',
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',
'account.verification.create',
'account.verification.update',
'teams.membership.create',
'teams.membership.update',
'teams.membership.delete',
];
$logs = $audit->getLogsByUserAndEvents($user->getId(), $auditEvents, $limit, $offset);
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
$output = [];
@ -1249,7 +1270,7 @@ App::get('/v1/account/logs')
;
$response->dynamic(new Document([
'total' => $audit->countLogsByUserAndEvents($user->getId(), $auditEvents),
'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@ -1309,7 +1330,7 @@ App::get('/v1/account/sessions/:sessionId')
App::patch('/v1/account/name')
->desc('Update Account Name')
->groups(['api', 'account'])
->label('event', 'account.update.name')
->label('event', 'users.[userId].update.name')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
@ -1324,12 +1345,14 @@ App::patch('/v1/account/name')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($name, $response, $user, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($name, $response, $user, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Stats\Stats $events */
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('name', $name)
@ -1338,21 +1361,27 @@ App::patch('/v1/account/name')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.name')
->setParam('resource', 'user/' . $user->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/account/password')
->desc('Update Account Password')
->groups(['api', 'account'])
->label('event', 'account.update.password')
->label('event', 'users.[userId].update.password')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
@ -1368,12 +1397,14 @@ App::patch('/v1/account/password')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($password, $oldPassword, $response, $user, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($password, $oldPassword, $response, $user, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Stats\Stats $events */
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
@ -1387,20 +1418,27 @@ App::patch('/v1/account/password')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.password')
->setParam('resource', 'user/' . $user->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/account/email')
->desc('Update Account Email')
->groups(['api', 'account'])
->label('event', 'account.update.email')
->label('event', 'users.[userId].update.email')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
@ -1416,12 +1454,14 @@ App::patch('/v1/account/email')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($email, $password, $response, $user, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($email, $password, $response, $user, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Stats\Stats $events */
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
@ -1452,20 +1492,27 @@ App::patch('/v1/account/email')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.update.email')
->setParam('resource', 'user/' . $user->getId())
->setUser($user)
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/account/prefs')
->desc('Update Account Preferences')
->groups(['api', 'account'])
->label('event', 'account.update.prefs')
->label('event', 'users.[userId].update.prefs')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
@ -1480,30 +1527,39 @@ App::patch('/v1/account/prefs')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($prefs, $response, $user, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($prefs, $response, $user, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$audits
->setParam('event', 'account.update.prefs')
->setParam('resource', 'user/' . $user->getId())
->setParam('userId', $user->getId())
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::delete('/v1/account')
->desc('Delete Account')
->groups(['api', 'account'])
->label('event', 'account.delete')
->label('event', 'users.[userId].delete')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
@ -1534,11 +1590,11 @@ App::delete('/v1/account')
// TODO delete all tokens or only current session?
// TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later
/*
* Data to delete
* * Tokens
* * Memberships
*/
/**
* Data to delete
* * Tokens
* * Memberships
*/
$audits
->setParam('userId', $user->getId())
@ -1548,7 +1604,8 @@ App::delete('/v1/account')
;
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_USER))
;
if (!Config::getParam('domainVerification')) {
@ -1571,7 +1628,7 @@ App::delete('/v1/account/sessions/:sessionId')
->desc('Delete Account Session')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteSession')
@ -1640,7 +1697,9 @@ App::delete('/v1/account/sessions/:sessionId')
$dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $sessions));
$events
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
->setPayload($response->output($session, Response::MODEL_SESSION))
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
$usage
@ -1658,7 +1717,7 @@ App::patch('/v1/account/sessions/:sessionId')
->desc('Update Session (Refresh Tokens)')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.sessions.update')
->label('event', 'users.[userId].sessions.[sessionId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateSession')
@ -1741,7 +1800,9 @@ App::patch('/v1/account/sessions/:sessionId')
;
$events
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
->setPayload($response->output($session, Response::MODEL_SESSION))
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
$usage
@ -1760,7 +1821,7 @@ App::delete('/v1/account/sessions')
->desc('Delete All Account Sessions')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteSessions')
@ -1823,10 +1884,12 @@ App::delete('/v1/account/sessions')
$numOfSessions = count($sessions);
$events
->setParam('eventData', $response->output(new Document([
->setPayload($response->output(new Document([
'sessions' => $sessions,
'total' => $numOfSessions,
]), Response::MODEL_SESSION_LIST))
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
$usage
@ -1840,7 +1903,7 @@ App::post('/v1/account/recovery')
->desc('Create Password Recovery')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.recovery.create')
->label('event', 'users.[userId].recovery.[tokenId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createRecovery')
@ -1915,14 +1978,15 @@ App::post('/v1/account/recovery')
$url = Template::unParseURL($url);
$mails
->setParam('event', 'account.recovery.create')
->setParam('from', $project->getId())
->setParam('recipient', $profile->getAttribute('email', ''))
->setParam('name', $profile->getAttribute('name', ''))
->setParam('url', $url)
->setParam('locale', $locale->default)
->setParam('project', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('type', MAIL_TYPE_RECOVERY)
->setPayload([
'from' => $project->getId(),
'recipient' => $profile->getAttribute('email', ''),
'name' => $profile->getAttribute('name', ''),
'url' => $url,
'locale' => $locale->default,
'project' => $project->getAttribute('name', ['[APP-NAME]']),
'type' => MAIL_TYPE_RECOVERY
])
->trigger();
;
@ -1931,6 +1995,8 @@ App::post('/v1/account/recovery')
$response->output($recovery->setAttribute('secret', $secret),
Response::MODEL_TOKEN
))
->setParam('userId', $profile->getId())
->setParam('tokenId', $recovery->getId())
;
$recovery // Hide secret for clients, sp
@ -1954,7 +2020,7 @@ App::put('/v1/account/recovery')
->desc('Create Password Recovery (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.recovery.update')
->label('event', 'users.[userId].recovery.[tokenId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateRecovery')
@ -1972,11 +2038,13 @@ App::put('/v1/account/recovery')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($userId, $secret, $password, $passwordAgain, $response, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($userId, $secret, $password, $passwordAgain, $response, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
if ($password !== $passwordAgain) {
throw new Exception('Passwords must match', 400, Exception::USER_PASSWORD_MISMATCH);
@ -2025,6 +2093,11 @@ App::put('/v1/account/recovery')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $profile->getId())
->setParam('tokenId', $recovery->getId())
;
$response->dynamic($recovery, Response::MODEL_TOKEN);
});
@ -2032,7 +2105,7 @@ App::post('/v1/account/verification')
->desc('Create Email Verification')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.verification.create')
->label('event', 'users.[userId].verification.[tokenId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createVerification')
@ -2098,14 +2171,15 @@ App::post('/v1/account/verification')
$url = Template::unParseURL($url);
$mails
->setParam('event', 'account.verification.create')
->setParam('from', $project->getId())
->setParam('recipient', $user->getAttribute('email'))
->setParam('name', $user->getAttribute('name'))
->setParam('url', $url)
->setParam('locale', $locale->default)
->setParam('project', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('type', MAIL_TYPE_VERIFICATION)
->setPayload([
'from' => $project->getId(),
'recipient' => $user->getAttribute('email'),
'name' => $user->getAttribute('name'),
'url' => $url,
'locale' => $locale->default,
'project' => $project->getAttribute('name', ['[APP-NAME]']),
'type' => MAIL_TYPE_VERIFICATION
])
->trigger()
;
@ -2114,6 +2188,8 @@ App::post('/v1/account/verification')
$response->output($verification->setAttribute('secret', $verificationSecret),
Response::MODEL_TOKEN
))
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
;
$verification // Hide secret for clients, sp
@ -2137,7 +2213,7 @@ App::put('/v1/account/verification')
->desc('Create Email Verification (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.verification.update')
->label('event', 'users.[userId].verification.[tokenId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateVerification')
@ -2154,12 +2230,14 @@ App::put('/v1/account/verification')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($userId, $secret, $response, $user, $dbForProject, $audits, $usage) {
->inject('events')
->action(function ($userId, $secret, $response, $user, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$profile = $dbForProject->getDocument('users', $userId);
@ -2200,5 +2278,11 @@ App::put('/v1/account/verification')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
;
$response->dynamic($verification, Response::MODEL_TOKEN);
});

View file

@ -2,6 +2,7 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Event\Validator\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
@ -585,7 +586,7 @@ App::post('/v1/projects/:projectId/webhooks')
->label('sdk.response.model', Response::MODEL_WEBHOOK)
->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('events', null, new ArrayList(new Event()), 'Events list.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
@ -707,7 +708,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('projectId', null, new UID(), 'Project unique ID.')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('events', null, new ArrayList(new Event()), 'Events list.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)

View file

@ -25,7 +25,7 @@ use Utopia\Validator\Boolean;
App::post('/v1/users')
->desc('Create User')
->groups(['api', 'users'])
->label('event', 'users.create')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -41,10 +41,12 @@ App::post('/v1/users')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $email, $password, $name, $response, $dbForProject, $usage) {
->inject('events')
->action(function ($userId, $email, $password, $name, $response, $dbForProject, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$email = \strtolower($email);
@ -77,6 +79,10 @@ App::post('/v1/users')
->setParam('users.create', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
@ -356,7 +362,7 @@ App::get('/v1/users/:userId/logs')
App::patch('/v1/users/:userId/status')
->desc('Update User Status')
->groups(['api', 'users'])
->label('event', 'users.update.status')
->label('event', 'users.[userId].update.status')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -370,10 +376,12 @@ App::patch('/v1/users/:userId/status')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $status, $response, $dbForProject, $usage) {
->inject('events')
->action(function ($userId, $status, $response, $dbForProject, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -386,13 +394,18 @@ App::patch('/v1/users/:userId/status')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification')
->desc('Update Email Verification')
->groups(['api', 'users'])
->label('event', 'users.update.verification')
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -406,10 +419,12 @@ App::patch('/v1/users/:userId/verification')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $emailVerification, $response, $dbForProject, $usage) {
->inject('events')
->action(function ($userId, $emailVerification, $response, $dbForProject, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -422,13 +437,18 @@ App::patch('/v1/users/:userId/verification')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/name')
->desc('Update Name')
->groups(['api', 'users'])
->label('event', 'users.update.name')
->label('event', 'users.[userId].update.name')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -442,10 +462,12 @@ App::patch('/v1/users/:userId/name')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $name, $response, $dbForProject, $audits) {
->inject('events')
->action(function ($userId, $name, $response, $dbForProject, $audits, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -456,9 +478,13 @@ App::patch('/v1/users/:userId/name')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('name', $name));
$audits
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.name')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@ -467,7 +493,7 @@ App::patch('/v1/users/:userId/name')
App::patch('/v1/users/:userId/password')
->desc('Update Password')
->groups(['api', 'users'])
->label('event', 'users.update.password')
->label('event', 'users.[userId].update.password')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -481,10 +507,12 @@ App::patch('/v1/users/:userId/password')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $password, $response, $dbForProject, $audits) {
->inject('events')
->action(function ($userId, $password, $response, $dbForProject, $audits, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -499,9 +527,13 @@ App::patch('/v1/users/:userId/password')
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.password')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@ -510,7 +542,7 @@ App::patch('/v1/users/:userId/password')
App::patch('/v1/users/:userId/email')
->desc('Update Email')
->groups(['api', 'users'])
->label('event', 'users.update.email')
->label('event', 'users.[userId].update.email')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -524,10 +556,12 @@ App::patch('/v1/users/:userId/email')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $email, $response, $dbForProject, $audits) {
->inject('events')
->action(function ($userId, $email, $response, $dbForProject, $audits, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -548,10 +582,15 @@ App::patch('/v1/users/:userId/email')
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
}
$audits
->setPayload(array_merge($audits->getPayload(), [
'resource' => 'user/'.$user->getId()
]))
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.email')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@ -560,7 +599,7 @@ App::patch('/v1/users/:userId/email')
App::patch('/v1/users/:userId/prefs')
->desc('Update User Preferences')
->groups(['api', 'users'])
->label('event', 'users.update.prefs')
->label('event', 'users.[userId].update.prefs')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -574,10 +613,12 @@ App::patch('/v1/users/:userId/prefs')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $prefs, $response, $dbForProject, $usage) {
->inject('events')
->action(function ($userId, $prefs, $response, $dbForProject, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$user = $dbForProject->getDocument('users', $userId);
@ -590,13 +631,18 @@ App::patch('/v1/users/:userId/prefs')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete User Session')
->groups(['api', 'users'])
->label('event', 'users.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -623,36 +669,36 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
}
$sessions = $user->getAttribute('sessions', []);
$key = array_search($sessionId, array_column($sessions, '$id'), true);
foreach ($sessions as $key => $session) { /** @var Document $session */
if ($sessionId == $session->getId()) {
unset($sessions[$key]);
$dbForProject->deleteDocument('sessions', $session->getId());
$user->setAttribute('sessions', $sessions);
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
$dbForProject->updateDocument('users', $user->getId(), $user);
}
if (!$key) {
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
}
$dbForProject->deleteDocument('sessions', $sessions[$key]->getId());
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
unset($sessions[$key]);
$user->setAttribute('sessions', $sessions);
$dbForProject->updateDocument('users', $user->getId(), $user);
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->noContent();
});
App::delete('/v1/users/:userId/sessions')
->desc('Delete User Sessions')
->groups(['api', 'users'])
->label('event', 'users.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -687,19 +733,21 @@ App::delete('/v1/users/:userId/sessions')
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
->setParam('userId', $user->getId())
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$response->noContent();
});
App::delete('/v1/users/:userId')
->desc('Delete User')
->groups(['api', 'users'])
->label('event', 'users.delete')
->label('event', 'users.[userId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@ -719,7 +767,7 @@ App::delete('/v1/users/:userId')
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
@ -730,7 +778,7 @@ App::delete('/v1/users/:userId')
* DO NOT DELETE THE USER RECORD ITSELF.
* WE RETAIN THE USER RECORD TO RESERVE THE USER ID AND ENSURE THAT THE USER ID IS NOT REUSED.
*/
// clone user object to send to workers
$clone = clone $user;
@ -752,6 +800,7 @@ App::delete('/v1/users/:userId')
$events
->setParam('eventData', $response->output($clone, Response::MODEL_USER))
->setParam('userId', $user->getId())
;
$usage

View file

@ -1,6 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Utopia\App;
use Appwrite\Extend\Exception;
@ -87,28 +88,34 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
/*
* Background Jobs
*/
$events
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
$events
->setParam('projectId', $project->getId())
->setParam('webhooks', $project->getAttribute('webhooks', []))
->setParam('userId', $user->getId())
->setParam('event', $route->getLabel('event', ''))
->setParam('eventData', [])
->setParam('functionId', null)
->setParam('executionId', null)
->setParam('functionId', null)
->setParam('executionId', null)
->setParam('trigger', 'event')
;
$audits
->setParam('projectId', $project->getId())
->setParam('userId', $user->getId())
->setParam('userEmail', $user->getAttribute('email'))
->setParam('userName', $user->getAttribute('name'))
->setParam('mode', $mode)
->setParam('event', '')
->setParam('resource', '')
->setParam('userAgent', $request->getUserAgent(''))
->setParam('ip', $request->getIP())
->setParam('data', [])
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
->setPayload([
'mode' => $mode,
'userAgent' => $request->getUserAgent(''),
'ip' => $request->getIP(),
'data' => [],
'resource' => ''
])
;
$usage
@ -196,19 +203,30 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
/** @var Appwrite\Event\Event $database */
/** @var bool $mode */
if (!empty($events->getEvent())) {
$allEvents = Event::generateEvents($events->getEvent(), $events->getParams());
var_dump($request->getRoute()->getPath(), $events->getEvent(), $allEvents);
foreach ($project->getAttribute('webhooks', []) as $webhook) {
/**
* @var Document $webhook
*/
if (array_intersect($webhook->getAttribute('events', []), $allEvents)) {
$events
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setTrigger($webhook)
->setPayload($response->getPayload())
->trigger();
}
}
}
if (!empty($events->getParam('event'))) {
if (empty($events->getParam('eventData'))) {
$events->setParam('eventData', $response->getPayload());
}
$webhooks = clone $events;
$functions = clone $events;
$webhooks
->setQueue('v1-webhooks')
->setClass('WebhooksV1')
->trigger();
$functions
->setQueue('v1-functions')
->setClass('FunctionsV1')
@ -241,7 +259,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
}
}
if (!empty($audits->getParam('event'))) {
if (!empty($audits->getEvent())) {
$audits->trigger();
}

View file

@ -362,10 +362,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Pub/sub error: ' . $th->getMessage());
$register->get('redisPool')->put($redis);
$attempts++;
sleep(DATABASE_RECONNECT_SLEEP);
continue;
}
$attempts++;
}
Console::error('Failed to restart pub/sub...');

View file

@ -1,8 +1,10 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Resque\Worker;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Database\Document;
require_once __DIR__.'/../init.php';
@ -21,21 +23,24 @@ class AuditsV1 extends Worker
public function run(): void
{
$projectId = $this->args['projectId'];
$userId = $this->args['userId'];
$userName = $this->args['userName'];
$userEmail = $this->args['userEmail'];
$mode = $this->args['mode'];
$event = $this->args['event'];
$resource = $this->args['resource'];
$userAgent = $this->args['userAgent'];
$ip = $this->args['ip'];
$data = $this->args['data'];
$dbForProject = $this->getProjectDB($projectId);
$audit = new Audit($dbForProject);
$events = $this->args['events'];
$user = new Document($this->args['user']);
$project = new Document($this->args['project']);
$payload = $this->args['payload'];
$audit->log($userId, $event, $resource, $userAgent, $ip, '', [
$userName = $user->getAttribute('name', '');
$userEmail = $user->getAttribute('email', '');
$event = $events[0];
$mode = $payload['mode'];
$resource = $payload['resource'];
$userAgent = $payload['userAgent'];
$ip = $payload['ip'];
$data = $payload['data'];
$dbForProject = $this->getProjectDB($project->getId());
$audit = new Audit($dbForProject);
$audit->log($user->getId(), $event, $resource, $userAgent, $ip, '', [
'userName' => $userName,
'userEmail' => $userEmail,
'mode' => $mode,

View file

@ -4,6 +4,7 @@ use Appwrite\Resque\Worker;
use Appwrite\Template\Template;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Locale\Locale;
require_once __DIR__ . '/../init.php';
@ -30,26 +31,28 @@ class MailsV1 extends Worker
return;
}
$recipient = $this->args['recipient'];
$name = $this->args['name'];
$url = $this->args['url'];
$project = $this->args['project'];
$type = $this->args['type'];
$payload = $this->args['payload'];
$recipient = $payload['recipient'];
$url = $payload['url'];
$project = $payload['project'];
$type = $payload['type'];
$name = $payload['name'] ?? $recipient;
$prefix = $this->getPrefix($type);
$locale = new Locale($this->args['locale']);
$locale = new Locale($payload['locale']);
if (!$this->doesLocaleExist($locale, $prefix)) {
$locale->setDefault('en');
}
$from = $this->args['from'] === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $project);
$from = $payload['from'] === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $project);
$body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl');
$subject = '';
switch ($type) {
case MAIL_TYPE_INVITATION:
$subject = \sprintf($locale->getText("$prefix.subject"), $this->args['team'], $project);
$body->setParam('{{owner}}', $this->args['owner']);
$body->setParam('{{team}}', $this->args['team']);
$subject = \sprintf($locale->getText("$prefix.subject"), $payload['team'], $project);
$body->setParam('{{owner}}', $payload['owner']);
$body->setParam('{{team}}', $payload['team']);
break;
case MAIL_TYPE_RECOVERY:
case MAIL_TYPE_VERIFICATION:

View file

@ -3,15 +3,17 @@
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Webhooks V1 Worker');
Console::success(APP_NAME . ' webhooks worker v1 has started');
class WebhooksV1 extends Worker
{
public function getName(): string {
public function getName(): string
{
return "webhooks";
}
@ -24,68 +26,57 @@ class WebhooksV1 extends Worker
$errors = [];
// Event
$projectId = $this->args['projectId'] ?? '';
$webhooks = $this->args['webhooks'] ?? [];
$userId = $this->args['userId'] ?? '';
$event = $this->args['event'] ?? '';
$eventData = \json_encode($this->args['eventData']);
$events = $this->args['events'];
$user = new Document($this->args['user']);
$project = new Document($this->args['project']);
$webhook = new Document($this->args['trigger'] ?? []);
$payload = \json_encode($this->args['payload']);
foreach ($webhooks as $webhook) {
if (!(isset($webhook['events']) && \is_array($webhook['events']) && \in_array($event, $webhook['events']))) {
continue;
}
$httpUser = $webhook->getAttribute('httpUser');
$httpPass = $webhook->getAttribute('httpPass');
$id = $webhook['$id'] ?? '';
$name = $webhook['name'] ?? '';
$signature = $webhook['signature'] ?? 'not-yet-implemented';
$url = $webhook['url'] ?? '';
$security = (bool) ($webhook['security'] ?? true);
$httpUser = $webhook['httpUser'] ?? null;
$httpPass = $webhook['httpPass'] ?? null;
$ch = \curl_init($webhook->getAttribute('url'));
$ch = \curl_init($url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
));
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . \strlen($payload),
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
'X-' . APP_NAME . '-Webhook-Event: ' . implode(', ', $events),
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
'X-' . APP_NAME . '-Webhook-Signature: ' . $webhook->getAttribute('signature') ?? 'not-yet-implemented',
]
);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $eventData);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
));
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . \strlen($eventData),
'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,
]
);
if (!$security) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
if (!empty($httpUser) && !empty($httpPass)) {
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
}
if (false === \curl_exec($ch)) {
$errors[] = \curl_error($ch) . ' in event ' . $event . ' for webhook ' . $name;
}
\curl_close($ch);
if (!$webhook->getAttribute('security', true)) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
if (!empty($httpUser) && !empty($httpPass)) {
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
}
if (false === \curl_exec($ch)) {
$errors[] = \curl_error($ch) . ' in events ' . implode(', ', $events) . ' for webhook ' . $webhook->getAttribute('name');
}
\curl_close($ch);
if (!empty($errors)) {
throw new Exception(\implode(" / \n\n", $errors));
}

View file

@ -5,6 +5,7 @@ namespace Appwrite\Event;
use Exception;
use InvalidArgumentException;
use Resque;
use Utopia\Database\Document;
class Event
{
@ -37,7 +38,12 @@ class Event
protected string $queue = '';
protected string $class = '';
protected string $event = '';
protected array $params = [];
protected array $payload = [];
protected ?Document $project = null;
protected ?Document $user = null;
protected ?Document $trigger = null;
/**
* @param string $queue
@ -59,6 +65,7 @@ class Event
public function setQueue(string $queue): self
{
$this->queue = $queue;
return $this;
}
@ -72,6 +79,76 @@ class Event
return $this->queue;
}
/**
* Set event name used for this event.
* @param string $event
* @return Event
*/
public function setEvent(string $event): self
{
$this->event = $event;
return $this;
}
/**
* Get event name used for this event.
*
* @return string
*/
public function getEvent(): string
{
return $this->event;
}
public function setProject(Document $project): self
{
$this->projectId = $project;
return $this;
}
public function getProjectId(): Document
{
return $this->projectId;
}
public function setUser(Document $user): self
{
$this->userId = $user;
return $this;
}
public function getUserId(): Document
{
return $this->userId;
}
public function setPayload(array $payload): self
{
$this->payload = $payload;
return $this;
}
public function getPayload(): array
{
return $this->payload;
}
public function setTrigger(Document $trigger): self
{
$this->trigger = $trigger;
return $this;
}
public function getTrigger(): Document
{
return $this->trigger;
}
/**
* Set class used for this event.
* @param string $class
@ -80,6 +157,7 @@ class Event
public function setClass(string $class): self
{
$this->class = $class;
return $this;
}
@ -118,17 +196,31 @@ class Event
return $this->params[$key] ?? null;
}
/**
* Get all params of the event.
*
* @return array
*/
public function getParams(): array
{
return $this->params;
}
/**
* Execute Event.
*
* @return Event
* @return string|bool
* @throws InvalidArgumentException
*/
public function trigger(): self
public function trigger(): string|bool
{
Resque::enqueue($this->queue, $this->class, $this->params);
return $this->reset();
return Resque::enqueue($this->queue, $this->class, [
'project' => $this->projectId,
'user' => $this->userId,
'payload' => $this->payload,
'trigger' => $this->trigger,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
}
/**
@ -143,50 +235,64 @@ class Event
return $this;
}
static function generateEvents(string $pattern, array $params = []): array
public static function parseEventPattern(string $pattern): array
{
$parts = \explode('.', $pattern);
$count = \count($parts);
if ($count < 2 || $count > 6) {
throw new Exception("Patten incorrect.");
}
/**
* Identify all sestions of the pattern.
*/
$type = $parts[0];
$action = match ($count) {
2 => $parts[1],
3, 4 => $parts[2],
5, 6 => $parts[4]
};
$type = $parts[0] ?? false;
$resource = $parts[1] ?? false;
$hasSubResource = $count > 3 && \str_starts_with($parts[3], '[');
if ($count > 4) {
if ($hasSubResource) {
$subType = $parts[2];
$subResource = $parts[3];
if ($count === 6) {
$attribute = $parts[5];
}
}
if ($count > 2) {
$resource = $parts[1];
} else {
if ($count === 4) {
$attribute = $parts[3];
}
}
$subType ??= false;
$subResource ??= false;
$attribute ??= false;
$action = match (true) {
!$hasSubResource && $count > 2 => $parts[2],
$hasSubResource && $count > 4 => $parts[4],
default => false
};
return [
'type' => $type,
'resource' => $resource,
'subType' => $subType,
'subResource' => $subResource,
'action' => $action,
'attribute' => $attribute,
];
}
static function generateEvents(string $pattern, array $params = []): array
{
$params = \array_filter($params, fn($param) => !\is_array($param));
$paramKeys = \array_keys($params);
$paramValues = \array_values($params);
$patterns = [];
$resource ??= false;
$subResource ??= false;
$attribute ??= false;
if (empty($params) && ($type ?? false) && !$resource) {
return [$pattern];
}
$parsed = self::parseEventPattern($pattern);
$type = $parsed['type'];
$resource = $parsed['resource'];
$subType = $parsed['subType'];
$subResource = $parsed['subResource'];
$action = $parsed['action'];
$attribute = $parsed['attribute'];
if ($resource && !\in_array(\trim($resource, '[]'), $paramKeys)) {
throw new InvalidArgumentException("{$resource} is missing from the params.");
@ -244,6 +350,7 @@ class Event
* Remove [] from the events.
*/
$events = \array_map(fn (string $event) => \str_replace(['[', ']'], '', $event), $events);
$events = \array_unique($events);
return $events;
}

View file

@ -0,0 +1,172 @@
<?php
namespace Appwrite\Event\Validator;
use Utopia\Validator;
/**
* Password.
*
* Validates user password string
*/
class Event extends Validator
{
protected array $types = [
'users' => [
'subTypes' => [
'sessions',
'recovery',
'verification'
],
'attributes' => [
'email',
'name',
'password',
'status',
'prefs',
]
],
'collections' => [
'subTypes' => [
'documents',
'attributes',
'indexes'
]
],
'buckets' => [
'subTypes' => [
'files'
]
],
'teams' => [
'subTypes' => [
'memberships' => [
'attributes' => [
'status'
]
]
]
],
'functions' => [
'subTypes' => [
'deployments',
'executions'
]
],
];
protected array $actions = [
'create',
'update',
'delete'
];
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters';
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
$parts = \explode('.', $value);
$count = \count($parts);
if ($count < 2 || $count > 6) {
return false;
}
/**
* Identify all sestions of the pattern.
*/
$type = $parts[0] ?? false;
$resource = $parts[1] ?? false;
$hasSubResource = $count > 3 && \array_key_exists('subTypes', $this->types[$type]) && \in_array($parts[2], $this->types[$type]['subTypes']);
if (!$type || !$resource) {
return false;
}
if ($hasSubResource) {
$subType = $parts[2];
$subResource = $parts[3];
if ($count === 6) {
$attribute = $parts[5];
}
} else {
if ($count === 4) {
$attribute = $parts[3];
}
}
$subType ??= false;
$subResource ??= false;
$attribute ??= false;
$action = match (true) {
!$hasSubResource && $count > 2 => $parts[2],
$hasSubResource && $count > 4 => $parts[4],
default => false
};
if ($action && !\in_array($action, $this->actions)) {
return false;
}
if (!\in_array($type, \array_keys($this->types))) {
return false;
}
if ($subtype ?? false) {
if (!($subResource ?? false) || !\in_array($subType, $this->types[$type]['subTypes'])) {
return false;
}
}
if ($attribute ?? false) {
if (
(\array_key_exists('attributes', $this->types[$type]) && !\in_array($attribute, $this->types[$type]['attributes'])) ||
(($subType ?? false) && \array_key_exists('attributes', $this->types[$type]['subTypes'][$subType]) && !\in_array($attribute, $this->types[$type]['subTypes'][$subType]['attributes']))
) {
return false;
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -98,54 +98,11 @@ trait ProjectCustom
], [
'name' => 'Webhook Test',
'events' => [
'account.create',
'account.update.email',
'account.update.name',
'account.update.password',
'account.update.prefs',
'account.recovery.create',
'account.recovery.update',
'account.verification.create',
'account.verification.update',
'account.delete',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'database.collections.create',
'database.collections.update',
'database.collections.delete',
'database.attributes.create',
'database.attributes.delete',
'database.indexes.create',
'database.indexes.delete',
'database.documents.create',
'database.documents.update',
'database.documents.delete',
'functions.create',
'functions.update',
'functions.delete',
'functions.deployments.create',
'functions.deployments.update',
'functions.deployments.delete',
'functions.executions.create',
'functions.executions.update',
'storage.files.create',
'storage.files.update',
'storage.files.delete',
'storage.buckets.create',
'storage.buckets.update',
'storage.buckets.delete',
'users.create',
'users.update.prefs',
'users.update.status',
'users.delete',
'users.sessions.delete',
'teams.create',
'teams.update',
'teams.delete',
'teams.memberships.create',
'teams.memberships.update.status',
'teams.memberships.delete',
'collections.*',
'functions.*',
'buckets.*',
'teams.*',
'users.*'
],
'url' => 'http://request-catcher:5000/webhook',
'security' => false,

View file

@ -310,7 +310,8 @@ trait AccountBase
{
sleep(10);
$session = $data['session'] ?? '';
$sessionId = $data['sessionId'] ?? '';
$userId = $data['id'] ?? '';
/**
* Test for SUCCESS
*/
@ -326,8 +327,7 @@ trait AccountBase
$this->assertNotEmpty($response['body']['logs']);
$this->assertCount(2, $response['body']['logs']);
$this->assertIsNumeric($response['body']['total']);
$this->assertContains($response['body']['logs'][0]['event'], ['account.create', 'account.sessions.create']);
$this->assertContains($response['body']['logs'][0]['event'], ["users.{$userId}.create", "users.{$userId}.sessions.{$sessionId}.create"]);
$this->assertEquals($response['body']['logs'][0]['ip'], filter_var($response['body']['logs'][0]['ip'], FILTER_VALIDATE_IP));
$this->assertIsNumeric($response['body']['logs'][0]['time']);
@ -349,7 +349,7 @@ trait AccountBase
$this->assertEquals('--', $response['body']['logs'][0]['countryCode']);
$this->assertEquals('Unknown', $response['body']['logs'][0]['countryName']);
$this->assertContains($response['body']['logs'][1]['event'], ['account.create', 'account.sessions.create']);
$this->assertContains($response['body']['logs'][1]['event'], ["users.{$userId}.create", "users.{$userId}.sessions.{$sessionId}.create"]);
$this->assertEquals($response['body']['logs'][1]['ip'], filter_var($response['body']['logs'][1]['ip'], FILTER_VALIDATE_IP));
$this->assertIsNumeric($response['body']['logs'][1]['time']);

View file

@ -87,9 +87,14 @@ class EventTest extends TestCase
public function testGenerateEvents()
{
$event = Event::generateEvents('users.create');
$this->assertCount(1, $event);
$this->assertContains('users.create', $event);
$event = Event::generateEvents('users.[userId].create', [
'userId' => 'torsten'
]);
$this->assertCount(4, $event);
$this->assertContains('users.torsten.create', $event);
$this->assertContains('users.torsten', $event);
$this->assertContains('users.*.create', $event);
$this->assertContains('users.*', $event);
$event = Event::generateEvents('users.[userId].update.email', [
'userId' => 'torsten'

View file

@ -0,0 +1,51 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Event\Validator\Event;
use PHPUnit\Framework\TestCase;
class EventValidatorTest extends TestCase
{
protected ?Event $object = null;
public function setUp(): void
{
$this->object = new Event();
}
public function tearDown(): void
{
}
public function testValues()
{
$this->assertTrue($this->object->isValid('users.*.create'));
$this->assertTrue($this->object->isValid('users.torsten.update'));
$this->assertTrue($this->object->isValid('users.torsten'));
$this->assertTrue($this->object->isValid('users.*.update.email'));
$this->assertTrue($this->object->isValid('users.*.update'));
$this->assertTrue($this->object->isValid('users.*'));
$this->assertTrue($this->object->isValid('collections.chapters.documents.prolog.create'));
$this->assertTrue($this->object->isValid('collections.chapters.documents.prolog'));
$this->assertTrue($this->object->isValid('collections.chapters.documents.*.create'));
$this->assertTrue($this->object->isValid('collections.chapters.documents.*'));
$this->assertTrue($this->object->isValid('collections.*.documents.prolog.create'));
$this->assertTrue($this->object->isValid('collections.*.documents.prolog'));
$this->assertTrue($this->object->isValid('collections.*.documents.*.create'));
$this->assertTrue($this->object->isValid('collections.*.documents.*'));
$this->assertTrue($this->object->isValid('collections.*'));
$this->assertTrue($this->object->isValid('functions.*'));
$this->assertTrue($this->object->isValid('buckets.*'));
$this->assertTrue($this->object->isValid('teams.*'));
$this->assertTrue($this->object->isValid('users.*'));
$this->assertFalse($this->object->isValid(false));
$this->assertFalse($this->object->isValid(null));
$this->assertFalse($this->object->isValid(''));
$this->assertFalse($this->object->isValid('collections'));
$this->assertFalse($this->object->isValid('collections.*.unknown'));
$this->assertFalse($this->object->isValid('collections.*.documents.*.unknown'));
$this->assertFalse($this->object->isValid('users.torsten.unknown'));
}
}