Merge remote-tracking branch 'upstream/cl-1.4.x' into feat-add-encrypt-param
This commit is contained in:
commit
4ddb0ce6a7
49 changed files with 2902 additions and 30 deletions
2
.env
2
.env
|
@ -80,4 +80,6 @@ _APP_DOCKER_HUB_USERNAME=
|
|||
_APP_DOCKER_HUB_PASSWORD=
|
||||
_APP_CONSOLE_GITHUB_SECRET=
|
||||
_APP_CONSOLE_GITHUB_APP_ID=
|
||||
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
|
||||
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
|
||||
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
|
@ -145,6 +145,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
|||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-webhooks && \
|
||||
chmod +x /usr/local/bin/worker-migrations && \
|
||||
chmod +x /usr/local/bin/worker-usage
|
||||
|
||||
# Letsencrypt Permissions
|
||||
|
|
|
@ -242,7 +242,7 @@ $commonCollections = [
|
|||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
@ -689,6 +689,172 @@ $commonCollections = [
|
|||
],
|
||||
],
|
||||
|
||||
'identities' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('identities'),
|
||||
'name' => 'Identities',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('userInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('userId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('provider'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerUid'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerEmail'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 320,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerAccessToken'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['encrypt'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerAccessTokenExpiry'),
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerRefreshToken'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['encrypt'],
|
||||
],
|
||||
[
|
||||
// Used to store data from provider that may or may not be sensitive
|
||||
'$id' => ID::custom('secrets'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => false,
|
||||
'filters' => ['json', 'encrypt'],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_userInternalId_provider_providerUid'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['userInternalId', 'provider', 'providerUid'],
|
||||
'lengths' => [Database::LENGTH_KEY, 100, 385],
|
||||
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_provider_providerUid'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['provider', 'providerUid'],
|
||||
'lengths' => [100, 640],
|
||||
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_userId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['userId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_userInternalId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['userInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_provider'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['provider'],
|
||||
'lengths' => [100],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_providerUid'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['providerUid'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_providerEmail'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['providerEmail'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_providerAccessTokenExpiry'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['providerAccessTokenExpiry'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'teams' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('teams'),
|
||||
|
@ -2507,6 +2673,143 @@ $projectCollections = array_merge([
|
|||
],
|
||||
],
|
||||
],
|
||||
|
||||
'migrations' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('migrations'),
|
||||
'name' => 'Migrations',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('status'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('stage'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('source'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 8192,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('credentials'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 65536,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => false,
|
||||
'filters' => ['json', 'encrypt'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('resources'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('statusCounters'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 3000,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('resourceData'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 131070,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('errors'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 65535,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_status',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['status'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_stage',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['stage'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_source',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['source'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_fulltext_search'),
|
||||
'type' => Database::INDEX_FULLTEXT,
|
||||
'attributes' => ['search'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
]
|
||||
],
|
||||
],
|
||||
], $commonCollections);
|
||||
|
||||
$consoleCollections = array_merge([
|
||||
|
@ -3877,7 +4180,7 @@ $dbCollections = [
|
|||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -190,6 +190,11 @@ return [
|
|||
'description' => 'The current user session could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::USER_IDENTITY_NOT_FOUND => [
|
||||
'name' => Exception::USER_IDENTITY_NOT_FOUND,
|
||||
'description' => 'The identity could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::USER_UNAUTHORIZED => [
|
||||
'name' => Exception::USER_UNAUTHORIZED,
|
||||
'description' => 'The current user is not authorized to perform the requested action.',
|
||||
|
@ -659,4 +664,21 @@ return [
|
|||
'description' => 'Too many queries.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Migrations */
|
||||
Exception::MIGRATION_NOT_FOUND => [
|
||||
'name' => Exception::MIGRATION_NOT_FOUND,
|
||||
'description' => 'Migration with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::MIGRATION_ALREADY_EXISTS => [
|
||||
'name' => Exception::MIGRATION_ALREADY_EXISTS,
|
||||
'description' => 'Migration with the requested ID already exists.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MIGRATION_IN_PROGRESS => [
|
||||
'name' => Exception::MIGRATION_IN_PROGRESS,
|
||||
'description' => 'Migration is already in progress.',
|
||||
'code' => 409,
|
||||
],
|
||||
];
|
||||
|
|
|
@ -51,6 +51,8 @@ $admins = [
|
|||
'functions.write',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'migrations.read',
|
||||
'migrations.write',
|
||||
];
|
||||
|
||||
return [
|
||||
|
|
|
@ -76,4 +76,10 @@ return [ // List of publicly visible scopes
|
|||
'health.read' => [
|
||||
'description' => 'Access to read your project\'s health status',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read your project\'s migrations',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s migrations.',
|
||||
]
|
||||
];
|
||||
|
|
|
@ -212,4 +212,17 @@ return [
|
|||
'optional' => false,
|
||||
'icon' => '',
|
||||
],
|
||||
'migrations' => [
|
||||
'key' => 'migrations',
|
||||
'name' => 'Migrations',
|
||||
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite server.',
|
||||
'description' => '/docs/services/migrations.md',
|
||||
'controller' => 'api/migrations.php',
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
'tests' => true,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/migrations.png',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -17,6 +17,7 @@ use Appwrite\OpenSSL\OpenSSL;
|
|||
use Appwrite\Template\Template;
|
||||
use Appwrite\URL\URL as URLParser;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
|
@ -195,6 +196,14 @@ App::post('/v1/account')
|
|||
}
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, null);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
|
@ -622,6 +631,22 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
$failureRedirect(Exception::USER_MISSING_ID);
|
||||
}
|
||||
|
||||
$name = $oauth2->getUserName($accessToken);
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
|
||||
// Check if this identity is connected to a different user
|
||||
if (!$user->isEmpty()) {
|
||||
$userId = $user->getId();
|
||||
|
||||
$identitiesWithMatchingEmail = $dbForProject->find('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $userId),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$current = Auth::sessionVerify($sessions, Auth::$secret, $authDuration);
|
||||
|
@ -645,9 +670,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
}
|
||||
|
||||
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
|
||||
$name = $oauth2->getUserName($accessToken);
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
|
||||
if (empty($email)) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.');
|
||||
}
|
||||
|
@ -664,7 +686,19 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
$user->setAttributes($userWithEmail->getArrayCopy());
|
||||
}
|
||||
|
||||
if ($user === false || $user->isEmpty()) { // Last option -> create the user, generate random password
|
||||
// If user is not found, check if there is an identity with the same provider user ID
|
||||
if ($user === false || $user->isEmpty()) {
|
||||
$identity = $dbForProject->findOne('identities', [
|
||||
Query::equal('provider', [$provider]),
|
||||
Query::equal('providerUid', [$oauth2ID]),
|
||||
]);
|
||||
|
||||
if ($identity !== false && !$identity->isEmpty()) {
|
||||
$user = $dbForProject->getDocument('users', $identity->getAttribute('userId'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($user === false || $user->isEmpty()) { // Last option -> create the user
|
||||
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
|
||||
|
||||
if ($limit !== 0) {
|
||||
|
@ -675,11 +709,16 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
}
|
||||
}
|
||||
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = ID::unique();
|
||||
$password = Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
$user->setAttributes([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
|
@ -690,8 +729,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
'email' => $email,
|
||||
'emailVerification' => true,
|
||||
'status' => true, // Email should already be authenticated by OAuth2 provider
|
||||
'passwordHistory' => $passwordHistory > 0 ? [$password] : null,
|
||||
'password' => $password,
|
||||
'password' => null,
|
||||
'hash' => Auth::DEFAULT_ALGO,
|
||||
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||
'passwordUpdate' => null,
|
||||
|
@ -711,10 +749,54 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
}
|
||||
}
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
Authorization::setRole(Role::users()->toString());
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
$failureRedirect(Exception::USER_BLOCKED); // User is in status blocked
|
||||
}
|
||||
|
||||
$identity = $dbForProject->findOne('identities', [
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
Query::equal('provider', [$provider]),
|
||||
Query::equal('providerUid', [$oauth2ID]),
|
||||
]);
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
// Before creating the identity, check if the email is already associated with another user
|
||||
$userId = $user->getId();
|
||||
|
||||
$identitiesWithMatchingEmail = $dbForProject->find('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->createDocument('identities', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::user($userId)),
|
||||
Permission::delete(Role::user($userId)),
|
||||
],
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'userId' => $userId,
|
||||
'provider' => $provider,
|
||||
'providerUid' => $oauth2ID,
|
||||
'providerEmail' => $email,
|
||||
'providerAccessToken' => $accessToken,
|
||||
'providerRefreshToken' => $refreshToken,
|
||||
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
|
||||
]));
|
||||
} else {
|
||||
$identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
|
||||
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
// Create session token, verify user account and update OAuth2 ID and Access Token
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
|
@ -794,6 +876,86 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
;
|
||||
});
|
||||
|
||||
App::get('/v1/account/identities')
|
||||
->desc('List Identities')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'listIdentities')
|
||||
->label('sdk.description', '/docs/references/account/list-identities.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_IDENTITY_LIST)
|
||||
->label('sdk.offline.model', '/account/identities')
|
||||
->param('queries', [], new Identities(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Identities::ALLOWED_ATTRIBUTES), true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, Response $response, Document $user, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
$queries[] = Query::equal('userInternalId', [$user->getInternalId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$identityId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Identity '{$identityId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$results = $dbForProject->find('identities', $queries);
|
||||
$total = $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'identities' => $results,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_IDENTITY_LIST);
|
||||
});
|
||||
|
||||
App::delete('/v1/account/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
->label('audits.event', 'identity.delete')
|
||||
->label('audits.resource', 'identity/{request.$identityId}')
|
||||
->label('audits.userId', '{user.$id}')
|
||||
->label('usage.metric', 'identities.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'deleteIdentity')
|
||||
->label('sdk.description', '/docs/references/account/delete-identity.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('identityId', [], new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/magic-url')
|
||||
->desc('Create Magic URL session')
|
||||
->groups(['api', 'account'])
|
||||
|
@ -846,6 +1008,14 @@ App::post('/v1/account/sessions/magic-url')
|
|||
}
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$user->setAttributes([
|
||||
|
@ -1832,6 +2002,15 @@ App::patch('/v1/account/email')
|
|||
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
|
||||
|
|
974
app/controllers/api/migrations.php
Normal file
974
app/controllers/api/migrations.php
Normal file
|
@ -0,0 +1,974 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Host;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
include_once __DIR__ . '/../shared/api.php';
|
||||
|
||||
App::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Appwrite Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createAppwriteMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-appwrite.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
|
||||
->param('projectId', '', new UID(), "Source's Project ID")
|
||||
->param('apiKey', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'credentials' => [
|
||||
'endpoint' => $endpoint,
|
||||
'projectId' => $projectId,
|
||||
'apiKey' => $apiKey,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/firebase/oauth')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Firebase Data (OAuth)')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseOAuthMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->inject('request')
|
||||
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $events, Request $request) {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
if ($identity->getAttribute('secret')) {
|
||||
$serviceAccount = $identity->getAttribute('secret');
|
||||
} else {
|
||||
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
|
||||
$identity = $identity
|
||||
->setAttribute('secret', $serviceAccount);
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Firebase::getName(),
|
||||
'credentials' => [
|
||||
'serviceAccount' => json_encode($serviceAccount),
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => []
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/firebase')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Firebase Data (Service Account)')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Firebase::getName(),
|
||||
'credentials' => [
|
||||
'serviceAccount' => $serviceAccount,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/supabase')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Supabase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createSupabaseMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-supabase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint')
|
||||
->param('apiKey', '', new Text(512), 'Source\'s API Key')
|
||||
->param('databaseHost', '', new Text(512), 'Source\'s Database Host')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Supabase::getName(),
|
||||
'credentials' => [
|
||||
'endpoint' => $endpoint,
|
||||
'apiKey' => $apiKey,
|
||||
'databaseHost' => $databaseHost,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'port' => $port,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/nhost')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate NHost Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createNHostMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-nhost.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('subdomain', '', new URL(), 'Source\'s Subdomain')
|
||||
->param('region', '', new Text(512), 'Source\'s Region')
|
||||
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret')
|
||||
->param('database', '', new Text(512), 'Source\'s Database Name')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => NHost::getName(),
|
||||
'credentials' => [
|
||||
'subdomain' => $subdomain,
|
||||
'region' => $region,
|
||||
'adminSecret' => $adminSecret,
|
||||
'database' => $database,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'port' => $port,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('List Migrations')
|
||||
->label('scope', 'migrations.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'list')
|
||||
->label('sdk.description', '/docs/references/migrations/list-migrations.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_LIST)
|
||||
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$migrationId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Migration '{$migrationId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'migrations' => $dbForProject->find('migrations', $queries),
|
||||
'total' => $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_MIGRATION_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Get Migration')
|
||||
->label('scope', 'migrations.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'get')
|
||||
->label('sdk.description', '/docs/references/migrations/get-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('migrationId', '', new UID(), 'Migration unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/appwrite/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Appwrite Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getAppwriteReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-appwrite-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
|
||||
->param('projectID', '', new Text(512), "Source's Project ID")
|
||||
->param('key', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($appwrite->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Firebase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getFirebaseReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response) {
|
||||
try {
|
||||
$firebase = new Firebase(json_decode($serviceAccount, true));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($firebase->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/report/oauth')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Firebase Data using OAuth')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getFirebaseReportOAuth')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('projectId', '', new Text(65536), 'Project ID')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('user')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
|
||||
try {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
// Get Service Account
|
||||
if ($identity->getAttribute('secret')) {
|
||||
$serviceAccount = $identity->getAttribute('secret');
|
||||
} else {
|
||||
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
|
||||
$identity = $identity
|
||||
->setAttribute('secret', $serviceAccount);
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$firebase = new Firebase(array_merge($serviceAccount, ['project_id' => $projectId]));
|
||||
|
||||
$report = $firebase->report($resources);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/connect')
|
||||
->desc('Authorize with firebase')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('scope', 'public')
|
||||
->label('origin', '*')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseAuth')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
|
||||
->label('sdk.methodType', 'webAuth')
|
||||
->label('sdk.hide', true)
|
||||
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
|
||||
->param('projectId', '', new UID(), 'Project ID')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('user')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
|
||||
$state = \json_encode([
|
||||
'projectId' => $projectId,
|
||||
'redirect' => $redirect,
|
||||
]);
|
||||
|
||||
$prefs = $user->getAttribute('prefs', []);
|
||||
$prefs['migrationState'] = $state;
|
||||
$user->setAttribute('prefs', $prefs);
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
$oauth2 = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
$url = $oauth2->getLoginURL();
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($url);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/redirect')
|
||||
->desc('Capture and receive data on Firebase authorization')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('scope', 'public')
|
||||
->label('error', __DIR__ . '/../../views/general/error.phtml')
|
||||
->param('code', '', new Text(2048), 'OAuth2 code.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
|
||||
$state = $user['prefs']['migrationState'] ?? '{}';
|
||||
$prefs['migrationState'] = '';
|
||||
$user->setAttribute('prefs', $prefs);
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
if (empty($state)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
|
||||
}
|
||||
|
||||
$state = \json_decode($state, true);
|
||||
$redirect = $state['redirect'] ?? '';
|
||||
$projectId = $state['projectId'] ?? '';
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if (empty($redirect)) {
|
||||
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
|
||||
}
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirect);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth Authroization
|
||||
if (!empty($code)) {
|
||||
$oauth2 = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$accessToken = $oauth2->getAccessToken($code);
|
||||
$refreshToken = $oauth2->getRefreshToken($code);
|
||||
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
$oauth2ID = $oauth2->getUserID($accessToken);
|
||||
|
||||
if (empty($accessToken)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
|
||||
}
|
||||
|
||||
if (empty($refreshToken)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
|
||||
}
|
||||
|
||||
if (empty($accessTokenExpiry)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
|
||||
if ($identity !== false && !$identity->isEmpty()) {
|
||||
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if ($identity !== false && !$identity->isEmpty()) {
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
} else {
|
||||
$identity = $dbForConsole->createDocument('identities', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'userId' => $user->getId(),
|
||||
'provider' => 'firebase',
|
||||
'providerUid' => $oauth2ID,
|
||||
'providerEmail' => $email,
|
||||
'providerAccessToken' => $accessToken,
|
||||
'providerRefreshToken' => $refreshToken,
|
||||
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirect);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/projects')
|
||||
->desc('List Firebase Projects')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'listFirebaseProjects')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('request')
|
||||
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase');
|
||||
}
|
||||
|
||||
if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Missing Google OAuth credentials');
|
||||
}
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
try {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Failed to refresh Firebase access token');
|
||||
}
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$projects = $firebase->getProjects($accessToken);
|
||||
|
||||
$output = [];
|
||||
foreach ($projects as $project) {
|
||||
$output[] = [
|
||||
'displayName' => $project['displayName'],
|
||||
'projectId' => $project['projectId'],
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'projects' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/deauthorize')
|
||||
->desc('Revoke Appwrite\'s authorization to access Firebase Projects')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'deleteFirebaseAuth')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (Document $user, Response $response, Database $dbForConsole) {
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
|
||||
}
|
||||
|
||||
$dbForConsole->deleteDocument('identities', $identity->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/supabase/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Supabase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getSupabaseReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-supabase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint')
|
||||
->param('apiKey', '', new Text(512), 'Source\'s API Key')
|
||||
->param('databaseHost', '', new Text(512), 'Source\'s Database Host')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
|
||||
try {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($supabase->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/nhost/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on NHost Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getNHostReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-nhost-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('subdomain', '', new URL(), 'Source\'s Subdomain')
|
||||
->param('region', '', new Text(512), 'Source\'s Region')
|
||||
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret')
|
||||
->param('database', '', new Text(512), 'Source\'s Database Name')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
|
||||
try {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($nhost->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::patch('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Retry Migration')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].retry')
|
||||
->label('audits.event', 'migration.retry')
|
||||
->label('audits.resource', 'migrations/{request.migrationId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'retry')
|
||||
->label('sdk.description', '/docs/references/migrations/retry-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('migrationId', '', new UID(), 'Migration unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventInstance) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($migration->getAttribute('status') !== 'failed') {
|
||||
throw new Exception(Exception::MIGRATION_IN_PROGRESS, 'Migration not failed yet');
|
||||
}
|
||||
|
||||
$migration
|
||||
->setAttribute('status', 'pending')
|
||||
->setAttribute('dateUpdated', \time());
|
||||
|
||||
// Trigger Migration
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::delete('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Delete Migration')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].delete')
|
||||
->label('audits.event', 'migrationId.delete')
|
||||
->label('audits.resource', 'migrations/{request.migrationId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/functions/delete-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('migrationId', '', new UID(), 'Migration ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('deletes')
|
||||
->inject('events')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Delete $deletes, Event $events) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('migrations', $migration->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB', 500);
|
||||
}
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
|
@ -442,6 +442,14 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
}
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = ID::unique();
|
||||
$invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
|
||||
|
|
|
@ -9,6 +9,7 @@ use Appwrite\Event\Event;
|
|||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Users;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
|
@ -47,6 +48,14 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
|||
|
||||
if (!empty($email)) {
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -622,6 +631,53 @@ App::get('/v1/users/:userId/logs')
|
|||
]), Response::MODEL_LOG_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/users/identities')
|
||||
->desc('List Identities')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'listIdentities')
|
||||
->label('sdk.description', '/docs/references/users/list-identities.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_IDENTITY_LIST)
|
||||
->param('queries', [], new Identities(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Identities::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$identityId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'identities' => $dbForProject->find('identities', $queries),
|
||||
'total' => $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_IDENTITY_LIST);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/status')
|
||||
->desc('Update User Status')
|
||||
->groups(['api', 'users'])
|
||||
|
@ -898,6 +954,15 @@ App::patch('/v1/users/:userId/email')
|
|||
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
|
@ -1153,6 +1218,38 @@ App::delete('/v1/users/:userId')
|
|||
$response->noContent();
|
||||
});
|
||||
|
||||
App::delete('/v1/users/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'identity.delete')
|
||||
->label('audits.resource', 'identity/{request.$identityId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'deleteIdentity')
|
||||
->label('sdk.description', '/docs/references/users/delete-identity.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
->inject('deletes')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/users/usage')
|
||||
->desc('Get usage stats for the users API')
|
||||
->groups(['api', 'users', 'usage'])
|
||||
|
|
|
@ -1003,7 +1003,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
|
|||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
|
||||
|
||||
if ($projectId === 'console') {
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
|
||||
|
|
|
@ -470,6 +470,11 @@ class DeletesV1 extends Worker
|
|||
$this->deleteByGroup('tokens', [
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
// Delete identities
|
||||
$this->deleteByGroup('identities', [
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
308
app/workers/migrations.php
Normal file
308
app/workers/migrations.php
Normal file
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Role;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Migration\Destinations\Appwrite as DestinationsAppwrite;
|
||||
use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Source;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
Console::title('Migrations V1 Worker');
|
||||
Console::success(APP_NAME . ' Migrations worker v1 has started');
|
||||
|
||||
class MigrationsV1 extends Worker
|
||||
{
|
||||
/**
|
||||
* Database connection shared across all methods of this file
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
private Database $dbForProject;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'migrations';
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$type = $this->args['type'] ?? '';
|
||||
$events = $this->args['events'] ?? [];
|
||||
$project = new Document($this->args['project'] ?? []);
|
||||
$user = new Document($this->args['user'] ?? []);
|
||||
$payload = json_encode($this->args['payload'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Event execution.
|
||||
*/
|
||||
if (! empty($events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dbForProject = $this->getProjectDB(new Document($this->args['project']));
|
||||
|
||||
$this->processMigration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Source
|
||||
*
|
||||
* @return Source
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function processSource(string $source, array $credentials): Source
|
||||
{
|
||||
switch ($source) {
|
||||
case Firebase::getName():
|
||||
return new Firebase(
|
||||
json_decode($credentials['serviceAccount'], true),
|
||||
);
|
||||
break;
|
||||
case Supabase::getName():
|
||||
return new Supabase(
|
||||
$credentials['endpoint'],
|
||||
$credentials['apiKey'],
|
||||
$credentials['databaseHost'],
|
||||
'postgres',
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$credentials['port'],
|
||||
);
|
||||
break;
|
||||
case NHost::getName():
|
||||
return new NHost(
|
||||
$credentials['subdomain'],
|
||||
$credentials['region'],
|
||||
$credentials['adminSecret'],
|
||||
$credentials['database'],
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$credentials['port'],
|
||||
);
|
||||
break;
|
||||
case Appwrite::getName():
|
||||
return new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Invalid source type');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateMigrationDocument(Document $migration, Document $project): Document
|
||||
{
|
||||
/** Trigger Realtime */
|
||||
$allEvents = Event::generateEvents('migrations.[migrationId].update', [
|
||||
'migrationId' => $migration->getId(),
|
||||
]);
|
||||
|
||||
$target = Realtime::fromPayload(
|
||||
event: $allEvents[0],
|
||||
payload: $migration,
|
||||
project: $project
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $migration->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: $project->getId(),
|
||||
payload: $migration->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
);
|
||||
|
||||
return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration);
|
||||
}
|
||||
|
||||
protected function removeAPIKey(Document $apiKey)
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
|
||||
$consoleDB->deleteDocument('keys', $apiKey->getId());
|
||||
}
|
||||
|
||||
protected function generateAPIKey(Document $project): Document
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
$generatedSecret = bin2hex(\random_bytes(128));
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'name' => 'Transfer API Key',
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
'users.write',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
'databases.read',
|
||||
'databases.write',
|
||||
'collections.read',
|
||||
'collections.write',
|
||||
'documents.read',
|
||||
'documents.write',
|
||||
'buckets.read',
|
||||
'buckets.write',
|
||||
'files.read',
|
||||
'files.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
],
|
||||
'expire' => null,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => $generatedSecret,
|
||||
]);
|
||||
|
||||
$consoleDB->createDocument('keys', $key);
|
||||
$consoleDB->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Migration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function processMigration(): void
|
||||
{
|
||||
/**
|
||||
* @var Document $migrationDocument
|
||||
* @var Transfer $transfer
|
||||
*/
|
||||
$migrationDocument = null;
|
||||
$transfer = null;
|
||||
$projectDocument = $this->getConsoleDB()->getDocument('projects', $this->args['project']['$id']);
|
||||
$tempAPIKey = $this->generateAPIKey($projectDocument);
|
||||
|
||||
try {
|
||||
$migrationDocument = $this->dbForProject->getDocument('migrations', $this->args['migration']['$id']);
|
||||
$migrationDocument->setAttribute('stage', 'processing');
|
||||
$migrationDocument->setAttribute('status', 'processing');
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
|
||||
$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials'));
|
||||
|
||||
$source->report();
|
||||
|
||||
$destination = new DestinationsAppwrite(
|
||||
$projectDocument->getId(),
|
||||
'http://appwrite/v1',
|
||||
$tempAPIKey['secret'],
|
||||
);
|
||||
|
||||
$transfer = new Transfer(
|
||||
$source,
|
||||
$destination
|
||||
);
|
||||
|
||||
/** Start Transfer */
|
||||
$migrationDocument->setAttribute('stage', 'migrating');
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
$transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) {
|
||||
$migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache()));
|
||||
$migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
|
||||
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
});
|
||||
|
||||
$errors = $transfer->getReport(Resource::STATUS_ERROR);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
|
||||
$errorMessages = [];
|
||||
foreach ($errors as $error) {
|
||||
$errorMessages[] = "Failed to transfer resource '{$error['id']}:{$error['resource']}' with message '{$error['message']}'";
|
||||
}
|
||||
|
||||
$migrationDocument->setAttribute('errors', $errorMessages);
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationDocument->setAttribute('status', 'completed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
|
||||
if ($migrationDocument) {
|
||||
Console::error($th->getMessage());
|
||||
Console::error($th->getTraceAsString());
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
$migrationDocument->setAttribute('errors', [$th->getMessage()]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($transfer) {
|
||||
$errors = $transfer->getReport(Resource::STATUS_ERROR);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
$migrationDocument->setAttribute('errors', $errors);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($migrationDocument) {
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
}
|
||||
if ($tempAPIKey) {
|
||||
$this->removeAPIKey($tempAPIKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Verification
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function processVerification(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
}
|
10
bin/worker-migrations
Normal file
10
bin/worker-migrations
Normal file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=0.1 QUEUE='v1-migrations' APP_INCLUDE='/usr/src/code/app/workers/migrations.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
|
@ -74,7 +74,8 @@
|
|||
"adhocore/jwt": "1.1.2",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"slickdeals/statsd": "3.1.0",
|
||||
"league/csv": "9.7.1"
|
||||
"league/csv": "9.7.1",
|
||||
"utopia-php/migration": "^0.2.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
|
103
composer.lock
generated
103
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d8f45e92243913c5898f4c6b483ed770",
|
||||
"content-hash": "ca47a56f2285cea787de1f58a8ee968c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
@ -63,6 +63,47 @@
|
|||
],
|
||||
"time": "2021-02-20T09:56:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "appwrite/appwrite",
|
||||
"version": "8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-for-php.git",
|
||||
"reference": "2b9e966edf35c4061179ed98ea364698ab30de8b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2b9e966edf35c4061179ed98ea364698ab30de8b",
|
||||
"reference": "2b9e966edf35c4061179ed98ea364698ab30de8b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "3.7.35"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Appwrite\\": "src/Appwrite"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
|
||||
"support": {
|
||||
"email": "team@appwrite.io",
|
||||
"issues": "https://github.com/appwrite/sdk-for-php/issues",
|
||||
"source": "https://github.com/appwrite/sdk-for-php/tree/8.0.0",
|
||||
"url": "https://appwrite.io/support"
|
||||
},
|
||||
"time": "2023-04-12T10:16:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-clamav",
|
||||
"version": "2.0.0",
|
||||
|
@ -1920,6 +1961,64 @@
|
|||
},
|
||||
"time": "2023-02-07T05:42:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "9dc59bbe0d126e20434580a5aa7cae5793bab024"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/9dc59bbe0d126e20434580a5aa7cae5793bab024",
|
||||
"reference": "9dc59bbe0d126e20434580a5aa7cae5793bab024",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"appwrite/appwrite": "^8.0",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cli": "^0.15.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.10",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vlucas/phpdotenv": "^5.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Utopia\\Migration\\": "src/Migration"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Eldad Fux",
|
||||
"email": "eldad@appwrite.io"
|
||||
},
|
||||
{
|
||||
"name": "Bradley Schofield",
|
||||
"email": "bradley@appwrite.io"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to migrate resources between services.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"migration",
|
||||
"php",
|
||||
"upf",
|
||||
"utopia"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/0.2.0"
|
||||
},
|
||||
"time": "2023-08-09T16:28:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
"version": "0.2.0",
|
||||
|
@ -5278,5 +5377,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "8.0"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
|
|
|
@ -167,6 +167,8 @@ services:
|
|||
- _APP_GRAPHQL_MAX_DEPTH
|
||||
- _APP_CONSOLE_GITHUB_APP_ID
|
||||
- _APP_CONSOLE_GITHUB_SECRET
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
|
@ -582,6 +584,46 @@ services:
|
|||
- _APP_HAMSTER_TIME
|
||||
- _APP_MIXPANEL_TOKEN
|
||||
|
||||
appwrite-worker-migrations:
|
||||
entrypoint: worker-migrations
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-migrations
|
||||
restart: unless-stopped
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./tests:/usr/src/code/tests
|
||||
- ./vendor:/usr/src/code/tests
|
||||
depends_on:
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
- _APP_CONNECTIONS_DB_PROJECT
|
||||
- _APP_CONNECTIONS_DB_CONSOLE
|
||||
- _APP_CONNECTIONS_CACHE
|
||||
- _APP_CONNECTIONS_QUEUE
|
||||
- _APP_CONNECTIONS_PUBSUB
|
||||
|
||||
appwrite-maintenance:
|
||||
entrypoint: maintenance
|
||||
<<: *x-logging
|
||||
|
|
1
docs/references/account/delete-identity.md
Normal file
1
docs/references/account/delete-identity.md
Normal file
|
@ -0,0 +1 @@
|
|||
Delete an identity by its unique ID.
|
|
@ -1 +1 @@
|
|||
Get currently logged in user preferences as a key-value object.
|
||||
Get the preferences as a key-value object for the currently logged in user.
|
|
@ -1 +1 @@
|
|||
Get currently logged in user data as JSON object.
|
||||
Get the currently logged in user.
|
1
docs/references/account/list-identities.md
Normal file
1
docs/references/account/list-identities.md
Normal file
|
@ -0,0 +1 @@
|
|||
Get the list of identities for the currently logged in user.
|
|
@ -1 +1 @@
|
|||
Get currently logged in user list of latest security activity logs. Each log returns user IP address, location and date and time of log.
|
||||
Get the list of latest security activity logs for the currently logged in user. Each log returns user IP address, location and date and time of log.
|
|
@ -1 +1 @@
|
|||
Get currently logged in user list of active sessions across different devices.
|
||||
Get the list of active sessions across different devices for the currently logged in user.
|
1
docs/references/users/delete-identity.md
Normal file
1
docs/references/users/delete-identity.md
Normal file
|
@ -0,0 +1 @@
|
|||
Delete an identity by its unique ID.
|
1
docs/references/users/list-identities.md
Normal file
1
docs/references/users/list-identities.md
Normal file
|
@ -0,0 +1 @@
|
|||
Get identities for all users.
|
Binary file not shown.
Before Width: | Height: | Size: 458 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
|
@ -16,10 +16,16 @@ class Exception extends AppwriteException
|
|||
$this->message = $response;
|
||||
$decoded = json_decode($response, true);
|
||||
if (\is_array($decoded)) {
|
||||
if (\is_array($decoded['error'])) {
|
||||
$this->error = $decoded['error']['status'];
|
||||
$this->errorDescription = $decoded['error']['message'];
|
||||
$this->message = $this->error . ': ' . $this->errorDescription;
|
||||
} else {
|
||||
$this->error = $decoded['error'];
|
||||
$this->errorDescription = $decoded['error_description'];
|
||||
$this->message = $this->error . ': ' . $this->errorDescription;
|
||||
}
|
||||
}
|
||||
$type = match ($code) {
|
||||
400 => AppwriteException::USER_OAUTH2_BAD_REQUEST,
|
||||
401 => AppwriteException::USER_OAUTH2_UNAUTHORIZED,
|
||||
|
|
270
src/Appwrite/Auth/OAuth2/Firebase.php
Normal file
270
src/Appwrite/Auth/OAuth2/Firebase.php
Normal file
|
@ -0,0 +1,270 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
|
||||
class Firebase extends OAuth2
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'https://www.googleapis.com/auth/firebase',
|
||||
'https://www.googleapis.com/auth/datastore',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
'https://www.googleapis.com/auth/identitytoolkit',
|
||||
'https://www.googleapis.com/auth/userinfo.profile'
|
||||
];
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'firebase';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([
|
||||
'access_type' => 'offline',
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'state' => \json_encode($this->state),
|
||||
'response_type' => 'code',
|
||||
'prompt' => 'consent',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
'https://oauth2.googleapis.com/token',
|
||||
[],
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'client_secret' => $this->appSecret,
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code'
|
||||
])
|
||||
);
|
||||
|
||||
$this->tokens = \json_decode($response, true);
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
'https://oauth2.googleapis.com/token',
|
||||
[],
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken
|
||||
])
|
||||
);
|
||||
|
||||
$output = [];
|
||||
\parse_str($response, $output);
|
||||
$this->tokens = $output;
|
||||
|
||||
if (empty($this->tokens['refresh_token'])) {
|
||||
$this->tokens['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserID(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['id'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['email'] ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the OAuth email is verified
|
||||
*
|
||||
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if ($user['verified'] ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken)
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$response = $this->request(
|
||||
'GET',
|
||||
'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' . \urlencode($accessToken),
|
||||
[],
|
||||
);
|
||||
|
||||
$this->user = \json_decode($response, true);
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getProjects(string $accessToken): array
|
||||
{
|
||||
$projects = $this->request('GET', 'https://firebase.googleapis.com/v1beta1/projects', ['Authorization: Bearer ' . \urlencode($accessToken)]);
|
||||
|
||||
$projects = \json_decode($projects, true);
|
||||
|
||||
return $projects['results'];
|
||||
}
|
||||
|
||||
/*
|
||||
Be careful with the setIAMPolicy method, it will overwrite all existing policies
|
||||
**/
|
||||
public function assignIAMRoles(string $accessToken, string $email, string $projectId)
|
||||
{
|
||||
// Get IAM Roles
|
||||
$iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':getIamPolicy', [
|
||||
'Authorization: Bearer ' . \urlencode($accessToken),
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$iamRoles = \json_decode($iamRoles, true);
|
||||
|
||||
$iamRoles['bindings'][] = [
|
||||
'role' => 'roles/identitytoolkit.admin',
|
||||
'members' => [
|
||||
'serviceAccount:' . $email
|
||||
]
|
||||
];
|
||||
|
||||
$iamRoles['bindings'][] = [
|
||||
'role' => 'roles/firebase.admin',
|
||||
'members' => [
|
||||
'serviceAccount:' . $email
|
||||
]
|
||||
];
|
||||
|
||||
// Set IAM Roles
|
||||
$this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':setIamPolicy', [
|
||||
'Authorization: Bearer ' . \urlencode($accessToken),
|
||||
'Content-Type: application/json'
|
||||
], json_encode([
|
||||
'policy' => $iamRoles
|
||||
]));
|
||||
}
|
||||
|
||||
public function createServiceAccount(string $accessToken, string $projectId): array
|
||||
{
|
||||
// Create Service Account
|
||||
$response = $this->request(
|
||||
'POST',
|
||||
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
|
||||
[
|
||||
'Authorization: Bearer ' . \urlencode($accessToken),
|
||||
'Content-Type: application/json'
|
||||
],
|
||||
json_encode([
|
||||
'accountId' => 'appwrite-migrations',
|
||||
'serviceAccount' => [
|
||||
'displayName' => 'Appwrite Migrations'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$response = json_decode($response, true);
|
||||
|
||||
$this->assignIAMRoles($accessToken, $response['email'], $projectId);
|
||||
|
||||
// Create Service Account Key
|
||||
$responseKey = $this->request(
|
||||
'POST',
|
||||
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $response['email'] . '/keys',
|
||||
[
|
||||
'Authorization: Bearer ' . \urlencode($accessToken),
|
||||
'Content-Type: application/json'
|
||||
]
|
||||
);
|
||||
|
||||
$responseKey = json_decode($responseKey, true);
|
||||
|
||||
return json_decode(base64_decode($responseKey['privateKeyData']), true);
|
||||
}
|
||||
}
|
|
@ -38,6 +38,9 @@ class Event
|
|||
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
|
||||
public const MESSAGING_CLASS_NAME = 'MessagingV1';
|
||||
|
||||
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
|
||||
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
|
||||
|
||||
protected string $queue = '';
|
||||
protected string $class = '';
|
||||
protected string $event = '';
|
||||
|
|
98
src/Appwrite/Event/Migration.php
Normal file
98
src/Appwrite/Event/Migration.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use DateTime;
|
||||
use Resque;
|
||||
use ResqueScheduler;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Migration extends Event
|
||||
{
|
||||
protected string $type = '';
|
||||
protected ?Document $migration = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Event::MIGRATIONS_QUEUE_NAME, Event::MIGRATIONS_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets migration document for the migration event.
|
||||
*
|
||||
* @param Document $migration
|
||||
* @return self
|
||||
*/
|
||||
public function setMigration(Document $migration): self
|
||||
{
|
||||
$this->migration = $migration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set migration document for the function event.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getMigration(): ?Document
|
||||
{
|
||||
return $this->migration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets migration type for the migration event.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set migration type for the migration event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the migration event and sends it to the migrations worker.
|
||||
*
|
||||
* @return string|bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
return Resque::enqueue($this->queue, $this->class, [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'migration' => $this->migration
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the migration event and schedules it in the migrations worker queue.
|
||||
*
|
||||
* @param \DateTime|int $at
|
||||
* @return void
|
||||
* @throws \Resque_Exception
|
||||
* @throws \ResqueScheduler_InvalidTimestampException
|
||||
*/
|
||||
public function schedule(DateTime|int $at): void
|
||||
{
|
||||
ResqueScheduler::enqueueAt($at, $this->queue, $this->class, [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'migration' => $this->migration
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ class Exception extends \Exception
|
|||
* - Platform
|
||||
* - Domain
|
||||
* - GraphQL
|
||||
* - Migrations
|
||||
*/
|
||||
|
||||
/** General */
|
||||
|
@ -73,6 +74,7 @@ class Exception extends \Exception
|
|||
public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
public const USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
|
||||
public const USER_UNAUTHORIZED = 'user_unauthorized';
|
||||
public const USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported';
|
||||
public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
|
||||
|
@ -204,6 +206,11 @@ class Exception extends \Exception
|
|||
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||
public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
|
||||
|
||||
/** Migrations */
|
||||
public const MIGRATION_NOT_FOUND = 'migration_not_found';
|
||||
public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
|
||||
public const MIGRATION_IN_PROGRESS = 'migration_in_progress';
|
||||
|
||||
protected $type = '';
|
||||
protected $errors = [];
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ class V19 extends Migration
|
|||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
$this->alterPermissionIndex('_metadata');
|
||||
$this->alterUidType('_metadata');
|
||||
|
||||
Console::info('Migrating Databases');
|
||||
$this->migrateDatabases();
|
||||
|
@ -57,11 +58,13 @@ class V19 extends Migration
|
|||
$databaseTable = "database_{$database->getInternalId()}";
|
||||
|
||||
$this->alterPermissionIndex($databaseTable);
|
||||
$this->alterUidType($databaseTable);
|
||||
|
||||
foreach ($this->documentsIterator($databaseTable) as $collection) {
|
||||
$collectionTable = "{$databaseTable}_collection_{$collection->getInternalId()}";
|
||||
Console::log("Migrating Collections of {$collectionTable} {$collection->getId()} ({$collection->getAttribute('name')})");
|
||||
$this->alterPermissionIndex($collectionTable);
|
||||
$this->alterUidType($collectionTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +101,7 @@ class V19 extends Migration
|
|||
}
|
||||
if (!in_array($id, ['files', 'collections'])) {
|
||||
$this->alterPermissionIndex($id);
|
||||
$this->alterUidType($id);
|
||||
}
|
||||
|
||||
usleep(50000);
|
||||
|
@ -131,7 +135,7 @@ class V19 extends Migration
|
|||
protected function alterPermissionIndex($collectionName): void
|
||||
{
|
||||
try {
|
||||
$table = "`{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_{$collectionName}_perms";
|
||||
$table = "`{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_{$collectionName}_perms`";
|
||||
$this->pdo->prepare("
|
||||
ALTER TABLE {$table}
|
||||
DROP INDEX `_permission`,
|
||||
|
@ -142,6 +146,19 @@ class V19 extends Migration
|
|||
}
|
||||
}
|
||||
|
||||
protected function alterUidType($collectionName): void
|
||||
{
|
||||
try {
|
||||
$table = "`{$this->projectDB->getDefaultDatabase()}`.`_{$this->project->getInternalId()}_{$collectionName}`";
|
||||
$this->pdo->prepare("
|
||||
ALTER TABLE {$table}
|
||||
CHANGE COLUMN `_uid` `_uid` VARCHAR(255) NOT NULL ;
|
||||
")->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrating all Bucket tables.
|
||||
*
|
||||
|
@ -155,6 +172,7 @@ class V19 extends Migration
|
|||
$id = "bucket_{$bucket->getInternalId()}";
|
||||
Console::log("Migrating Bucket {$id} {$bucket->getId()} ({$bucket->getAttribute('name')})");
|
||||
$this->alterPermissionIndex($id);
|
||||
$this->alterUidType($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class Identities extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'userId',
|
||||
'provider',
|
||||
'providerUid',
|
||||
'providerEmail',
|
||||
'providerAccessTokenExpiry',
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('identities', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class Migrations extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'status',
|
||||
'stage',
|
||||
'source',
|
||||
'resources',
|
||||
'statusCounters',
|
||||
'resourceData',
|
||||
'errors'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('migrations', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ use Appwrite\Utopia\Response\Model\File;
|
|||
use Appwrite\Utopia\Response\Model\Bucket;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleVariables;
|
||||
use Appwrite\Utopia\Response\Model\Func;
|
||||
use Appwrite\Utopia\Response\Model\Identity;
|
||||
use Appwrite\Utopia\Response\Model\Index;
|
||||
use Appwrite\Utopia\Response\Model\JWT;
|
||||
use Appwrite\Utopia\Response\Model\Key;
|
||||
|
@ -88,6 +89,9 @@ use Appwrite\Utopia\Response\Model\UsageProject;
|
|||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\Variable;
|
||||
use Appwrite\Utopia\Response\Model\Migration;
|
||||
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
|
||||
use Appwrite\Utopia\Response\Model\MigrationReport;
|
||||
|
||||
/**
|
||||
* @method int getStatusCode()
|
||||
|
@ -145,6 +149,8 @@ class Response extends SwooleResponse
|
|||
public const MODEL_USER_LIST = 'userList';
|
||||
public const MODEL_SESSION = 'session';
|
||||
public const MODEL_SESSION_LIST = 'sessionList';
|
||||
public const MODEL_IDENTITY = 'identity';
|
||||
public const MODEL_IDENTITY_LIST = 'identityList';
|
||||
public const MODEL_TOKEN = 'token';
|
||||
public const MODEL_JWT = 'jwt';
|
||||
public const MODEL_PREFERENCES = 'preferences';
|
||||
|
@ -198,6 +204,13 @@ class Response extends SwooleResponse
|
|||
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
|
||||
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
|
||||
|
||||
// Migrations
|
||||
public const MODEL_MIGRATION = 'migration';
|
||||
public const MODEL_MIGRATION_LIST = 'migrationList';
|
||||
public const MODEL_MIGRATION_REPORT = 'migrationReport';
|
||||
public const MODEL_MIGRATION_FIREBASE_PROJECT = 'firebaseProject';
|
||||
public const MODEL_MIGRATION_FIREBASE_PROJECT_LIST = 'firebaseProjectList';
|
||||
|
||||
// Project
|
||||
public const MODEL_PROJECT = 'project';
|
||||
public const MODEL_PROJECT_LIST = 'projectList';
|
||||
|
@ -265,6 +278,7 @@ class Response extends SwooleResponse
|
|||
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
|
||||
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
|
||||
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
|
||||
->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY))
|
||||
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG))
|
||||
->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE))
|
||||
->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET))
|
||||
|
@ -290,6 +304,8 @@ class Response extends SwooleResponse
|
|||
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
|
||||
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
|
||||
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
|
||||
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
|
||||
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
|
||||
// Entities
|
||||
->setModel(new Database())
|
||||
->setModel(new Collection())
|
||||
|
@ -319,6 +335,7 @@ class Response extends SwooleResponse
|
|||
->setModel(new Account())
|
||||
->setModel(new Preferences())
|
||||
->setModel(new Session())
|
||||
->setModel(new Identity())
|
||||
->setModel(new Token())
|
||||
->setModel(new JWT())
|
||||
->setModel(new Locale())
|
||||
|
@ -362,6 +379,9 @@ class Response extends SwooleResponse
|
|||
->setModel(new TemplateSMS())
|
||||
->setModel(new TemplateEmail())
|
||||
->setModel(new ConsoleVariables())
|
||||
->setModel(new Migration())
|
||||
->setModel(new MigrationReport())
|
||||
->setModel(new MigrationFirebaseProject())
|
||||
// Verification
|
||||
// Recovery
|
||||
// Tests (keep last)
|
||||
|
|
95
src/Appwrite/Utopia/Response/Model/Identity.php
Normal file
95
src/Appwrite/Utopia/Response/Model/Identity.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Identity extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Identity ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Identity creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Identity update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('userId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5bb8c16897e',
|
||||
])
|
||||
->addRule('provider', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Identity Provider.',
|
||||
'default' => '',
|
||||
'example' => 'email',
|
||||
])
|
||||
->addRule('providerUid', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID of the User in the Identity Provider.',
|
||||
'default' => '',
|
||||
'example' => '5e5bb8c16897e',
|
||||
])
|
||||
->addRule('providerEmail', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Email of the User in the Identity Provider.',
|
||||
'default' => '',
|
||||
'example' => 'user@example.com',
|
||||
])
|
||||
->addRule('providerAccessToken', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Identity Provider Access Token.',
|
||||
'default' => '',
|
||||
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
|
||||
])
|
||||
->addRule('providerAccessTokenExpiry', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'The date of when the access token expires in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('providerRefreshToken', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Identity Provider Refresh Token.',
|
||||
'default' => '',
|
||||
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Identity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_IDENTITY;
|
||||
}
|
||||
}
|
96
src/Appwrite/Utopia/Response/Model/Migration.php
Normal file
96
src/Appwrite/Utopia/Response/Model/Migration.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Migration extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Migration ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Variable creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Variable creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Migration status ( pending, processing, failed. completed ) ',
|
||||
'default' => '',
|
||||
'example' => 'pending',
|
||||
])
|
||||
->addRule('stage', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Migration stage ( init, processing, source-check, destination-check, migrating, finished )',
|
||||
'default' => '',
|
||||
'example' => 'init',
|
||||
])
|
||||
->addRule('source', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'A string containing the type of source of the migration.',
|
||||
'default' => '',
|
||||
'example' => 'Appwrite',
|
||||
])
|
||||
->addRule('resources', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Resources to migration.',
|
||||
'default' => [],
|
||||
'example' => ['user'],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('statusCounters', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'A group of counters that represent the total progress of the migration.',
|
||||
'default' => [],
|
||||
'example' => '{"Database": {"PENDING": 0, "SUCCESS": 1, "ERROR": 0, "SKIP": 0, "PROCESSING": 0, "WARNING": 0}}',
|
||||
])
|
||||
->addRule('resourceData', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'An array of objects containing the report data of the resources that were migrated.',
|
||||
'default' => [],
|
||||
'example' => '[{"resource":"Database","id":"public","status":"SUCCESS","message":""}]',
|
||||
])
|
||||
->addRule('errors', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'All errors that occurred during the migration process.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Migration';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_MIGRATION;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class MigrationFirebaseProject extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('projectId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Project ID.',
|
||||
'default' => '',
|
||||
'example' => 'my-project',
|
||||
])
|
||||
->addRule('displayName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Project display name.',
|
||||
'default' => '',
|
||||
'example' => 'My Project',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'MigrationFirebaseProject';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_MIGRATION_FIREBASE_PROJECT;
|
||||
}
|
||||
}
|
89
src/Appwrite/Utopia/Response/Model/MigrationReport.php
Normal file
89
src/Appwrite/Utopia/Response/Model/MigrationReport.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Migration\Resource;
|
||||
|
||||
class MigrationReport extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule(Resource::TYPE_USER, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of users to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_TEAM, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of teams to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_DATABASE, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of databases to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_DOCUMENT, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of documents to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_FILE, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of files to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_BUCKET, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of buckets to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_FUNCTION, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of functions to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule('size', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Size of files to be migrated in mb.',
|
||||
'default' => 0,
|
||||
'example' => 30000,
|
||||
])
|
||||
->addRule('version', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Version of the Appwrite instance to be migrated.',
|
||||
'default' => '',
|
||||
'example' => '1.4.0',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Migration Report';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_MIGRATION_REPORT;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue