1
0
Fork 0
mirror of synced 2024-07-06 15:11:21 +12:00

Merge pull request #5886 from appwrite/feat-master-to-1.4.x

Merge master into 1.4.x
This commit is contained in:
Eldad A. Fux 2023-07-27 13:56:42 +03:00 committed by GitHub
commit 0b30c7def8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 412 additions and 51 deletions

View file

@ -17,6 +17,10 @@
- Improve permission indexes [DB #248](https://github.com/utopia-php/database/pull/248)
- Validators back-ported to Utopia [#5439](https://github.com/appwrite/appwrite/pull/5439)
# Version 1.3.8
## Bugs
- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809)
# Version 1.3.7

View file

@ -66,7 +66,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
### Windows
@ -78,7 +78,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
#### PowerShell
@ -88,7 +88,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。

View file

@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
### Windows
@ -87,7 +87,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
#### PowerShell
@ -97,7 +97,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.3.7
appwrite/appwrite:1.3.8
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
@ -128,6 +128,12 @@ Choose from one of the providers below:
<br /><sub><b>Gitpod</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
<br /><sub><b>Akamai</b></sub></a>
</a>
</td>
</tr>
</table>

View file

@ -201,6 +201,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'oidc' => [
'name' => 'OpenID Connect',
'developers' => 'https://openid.net/connect/faq/',
'icon' => 'icon-oidc',
'enabled' => true,
'sandbox' => false,
'form' => 'oidc.phtml',
'beta' => false,
'mock' => false,
],
'okta' => [
'name' => 'Okta',
'developers' => 'https://developer.okta.com/',
@ -222,7 +232,7 @@ return [ // Ordered by ABC.
'mock' => false
],
'paypalSandbox' => [
'name' => 'PayPal',
'name' => 'PayPal Sandbox',
'developers' => 'https://developer.paypal.com/docs/api/overview/',
'icon' => 'icon-paypal',
'enabled' => true,
@ -292,7 +302,7 @@ return [ // Ordered by ABC.
'mock' => false,
],
'tradeshiftBox' => [
'name' => 'Tradeshift',
'name' => 'Tradeshift Sandbox',
'developers' => 'https://developers.tradeshift.com/docs/api',
'icon' => 'icon-tradeshiftbox',
'enabled' => true,

View file

@ -1473,7 +1473,7 @@ App::get('/v1/account/logs')
$audit = new EventAudit($dbForProject);
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
$logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];

View file

@ -566,7 +566,7 @@ App::get('/v1/databases/:databaseId/logs')
$output[$i] = new Document([
'event' => $log['event'],
'userId' => ID::custom($log['userId']),
'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
@ -917,7 +917,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
@ -2543,21 +2543,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$indexes = $collection->getAttribute('indexes');
// Search for index
$indexIndex = array_search($key, array_map(fn($idx) => $idx['key'], $indexes));
if ($indexIndex === false) {
$index = $collection->find('key', $key, 'indexes');
if (empty($index)) {
throw new Exception(Exception::INDEX_NOT_FOUND);
}
$index = $indexes[$indexIndex];
$index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId);
$response->dynamic($index, Response::MODEL_INDEX);
});
App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default'])
->desc('Delete Index')
@ -3157,7 +3151,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,

View file

@ -1062,7 +1062,7 @@ App::get('/v1/teams/:teamId/logs')
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,

View file

@ -579,7 +579,7 @@ App::get('/v1/users/:userId/logs')
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
$logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];

View file

@ -103,7 +103,7 @@ const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 506;
const APP_VERSION_STABLE = '1.3.7';
const APP_VERSION_STABLE = '1.3.8';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';

View file

@ -40,7 +40,7 @@ class AuditsV1 extends Worker
$dbForProject = $this->getProjectDB($project->getId());
$audit = new Audit($dbForProject);
$audit->log(
userId: $user->getId(),
userId: $user->getInternalId(),
// Pass first, most verbose event pattern
event: $event,
resource: $resource,
@ -48,6 +48,7 @@ class AuditsV1 extends Worker
ip: $ip,
location: '',
data: [
'userId' => $user->getId(),
'userName' => $userName,
'userEmail' => $userEmail,
'mode' => $mode,

37
composer.lock generated
View file

@ -3785,16 +3785,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.22.1",
"version": "1.23.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "65c39594fbd8c67abfc68bb323f86447bab79cc0"
"reference": "a2b24135c35852b348894320d47b3902a94bc494"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/65c39594fbd8c67abfc68bb323f86447bab79cc0",
"reference": "65c39594fbd8c67abfc68bb323f86447bab79cc0",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a2b24135c35852b348894320d47b3902a94bc494",
"reference": "a2b24135c35852b348894320d47b3902a94bc494",
"shasum": ""
},
"require": {
@ -3826,22 +3826,22 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.22.1"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.0"
},
"time": "2023-06-29T20:46:06+00:00"
"time": "2023-07-23T22:17:56+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.26",
"version": "9.2.27",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1"
"reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
"reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1",
"reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1",
"shasum": ""
},
"require": {
@ -3897,7 +3897,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26"
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27"
},
"funding": [
{
@ -3905,7 +3906,7 @@
"type": "github"
}
],
"time": "2023-03-06T12:58:08+00:00"
"time": "2023-07-26T13:44:30+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -5580,16 +5581,16 @@
},
{
"name": "twig/twig",
"version": "v3.6.1",
"version": "v3.7.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd"
"reference": "5cf942bbab3df42afa918caeba947f1b690af64b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd",
"reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/5cf942bbab3df42afa918caeba947f1b690af64b",
"reference": "5cf942bbab3df42afa918caeba947f1b690af64b",
"shasum": ""
},
"require": {
@ -5635,7 +5636,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.6.1"
"source": "https://github.com/twigphp/Twig/tree/v3.7.0"
},
"funding": [
{
@ -5647,7 +5648,7 @@
"type": "tidelift"
}
],
"time": "2023-06-08T12:52:13+00:00"
"time": "2023-07-26T07:16:09+00:00"
}
],
"aliases": [],

View file

@ -1,3 +1,3 @@
Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.
A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits).
A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits).

View file

@ -1 +1 @@
Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the Session ID argument, only the unique session ID provided is deleted.
Logout the user. Use 'current' as the session ID to logout on this device, use a session ID to logout on another device. If you're looking to logout the user on all devices, use [Delete Sessions](/docs/client/account#accountDeleteSessions) instead.

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12.9252 21.5275C8.89292 20.2678 5.94661 16.4881 5.94661 12.0787C5.94661 7.51187 8.89322 3.735 13.08 2.4725C13.5453 2.315 13.3902 2 12.9252 2C7.34221 2.1575 3 6.56687 3 12.0787C3 17.5906 7.34221 22 12.9252 22C13.3905 22 13.3905 21.685 12.9252 21.5275ZM7.80752 14.2837V13.4962C7.80752 9.09 11.2194 5.62219 15.5616 5.62219C19.5937 5.62219 20.8345 7.51188 20.9896 7.35438C21.1447 7.19688 19.5937 3.575 14.7861 3.575C10.447 3.575 7.03201 7.03969 7.03201 11.4491C7.03201 12.3941 7.18711 13.3387 7.49731 14.2834C7.65242 14.7559 7.80752 14.7562 7.80752 14.2837ZM11.0643 8.61406C13.08 7.66906 15.5616 7.66906 18.043 8.61406C19.7488 9.24406 20.6794 10.0312 20.8342 10.0312C20.989 10.0312 19.9036 8.14156 17.8875 7.35406C15.4065 6.40625 12.9252 6.88531 10.9089 8.45625C10.7538 8.61375 10.9089 8.77125 11.064 8.61375L11.0643 8.61406Z" fill="#0095D6"/>
</svg>

After

Width:  |  Height:  |  Size: 953 B

View file

@ -0,0 +1,296 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://openid.net/connect/faq/
class Oidc extends OAuth2
{
/**
* @var array
*/
protected array $scopes = [
'openid',
'profile',
'email',
];
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'oidc';
}
protected array $wellKnownConfiguration = [];
/**
* @return string
*/
public function getLoginURL(): string
{
return $this->getAuthorizationEndpoint() . '?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'state' => \json_encode($this->state),
'scope' => \implode(' ', $this->getScopes()),
'response_type' => 'code',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->getTokenEndpoint(),
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->getTokenEndpoint(),
$headers,
\http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'grant_type' => 'refresh_token'
])
), true);
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);
if (isset($user['sub'])) {
return $user['sub'];
}
return '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
if (isset($user['email'])) {
return $user['email'];
}
return '';
}
/**
* Check if the User email is verified
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
return $user['email_verified'] ?? false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
if (isset($user['name'])) {
return $user['name'];
}
return '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$headers = ['Authorization: Bearer ' . \urlencode($accessToken)];
$user = $this->request('GET', $this->getUserinfoEndpoint(), $headers);
$this->user = \json_decode($user, true);
}
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
*
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->getAppSecret();
return $secret['clientSecret'] ?? '';
}
/**
* Extracts the well known endpoint from the JSON stored in appSecret.
*
* @return string
*/
protected function getWellKnownEndpoint(): string
{
$secret = $this->getAppSecret();
return $secret['wellKnownEndpoint'] ?? '';
}
/**
* Extracts the authorization endpoint from the JSON stored in appSecret.
*
* If one is not provided, it will be retrieved from the well-known configuration.
*
* @return string
*/
protected function getAuthorizationEndpoint(): string
{
$secret = $this->getAppSecret();
$endpoint = $secret['authorizationEndpoint'] ?? '';
if (!empty($endpoint)) {
return $endpoint;
}
$wellKnownConfiguration = $this->getWellKnownConfiguration();
return $wellKnownConfiguration['authorization_endpoint'] ?? '';
}
/**
* Extracts the token endpoint from the JSON stored in appSecret.
*
* If one is not provided, it will be retrieved from the well-known configuration.
*
* @return string
*/
protected function getTokenEndpoint(): string
{
$secret = $this->getAppSecret();
$endpoint = $secret['tokenEndpoint'] ?? '';
if (!empty($endpoint)) {
return $endpoint;
}
$wellKnownConfiguration = $this->getWellKnownConfiguration();
return $wellKnownConfiguration['token_endpoint'] ?? '';
}
/**
* Extracts the userinfo endpoint from the JSON stored in appSecret.
*
* If one is not provided, it will be retrieved from the well-known configuration.
*
* @return string
*/
protected function getUserinfoEndpoint(): string
{
$secret = $this->getAppSecret();
$endpoint = $secret['userinfoEndpoint'] ?? '';
if (!empty($endpoint)) {
return $endpoint;
}
$wellKnownConfiguration = $this->getWellKnownConfiguration();
return $wellKnownConfiguration['userinfo_endpoint'] ?? '';
}
/**
* Get the well-known configuration using the well known endpoint
*/
protected function getWellKnownConfiguration(): array
{
if (empty($this->wellKnownConfiguration)) {
$response = $this->request('GET', $this->getWellKnownEndpoint());
$this->wellKnownConfiguration = \json_decode($response, true);
}
return $this->wellKnownConfiguration;
}
/**
* Decode the JSON stored in appSecret
*
* @return array
*/
protected function getAppSecret(): array
{
try {
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
throw new \Exception('Invalid secret');
}
return $secret;
}
}

View file

@ -63,6 +63,7 @@ abstract class Migration
'1.3.5' => 'V18',
'1.3.6' => 'V18',
'1.3.7' => 'V18',
'1.3.8' => 'V18',
'1.4.0' => 'V19',
];

View file

@ -133,6 +133,16 @@ class V18 extends Migration
Console::warning("'options' from {$id}: {$th->getMessage()}");
}
break;
case 'audit':
try {
/**
* Delete 'userInternalId' attribute
*/
$this->projectDB->deleteAttribute($id, 'userInternalId');
} catch (\Throwable $th) {
Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
}
break;
default:
break;
}
@ -195,6 +205,34 @@ class V18 extends Migration
Console::warning($th->getMessage());
}
break;
case 'audit':
/**
* Set the userId to the userInternalId and add userId to data
*/
try {
$userId = $document->getAttribute('userId');
$data = $document->getAttribute('data', []);
$mode = $data['mode'] ?? 'default';
$user = match ($mode) {
'admin' => $this->consoleDB->getDocument('users', $userId),
default => $this->projectDB->getDocument('users', $userId),
};
if ($user->isEmpty()) {
// The audit userId could already be an internal Id.
// Otherwise, the user could have been deleted.
// Nonetheless, there's nothing else we can do here.
break;
}
$internalId = $user->getInternalId();
$document->setAttribute('userId', $internalId);
$data = $document->getAttribute('data', []);
$data['userId'] = $user->getId();
$document->setAttribute('data', $data);
} catch (\Throwable $th) {
Console::warning($th->getMessage());
}
break;
}
return $document;

View file

@ -136,7 +136,7 @@ class Project extends Model
'type' => Response::MODEL_PROVIDER,
'description' => 'List of Providers.',
'default' => [],
'example' => new \stdClass(),
'example' => [new \stdClass()],
'array' => true,
])
->addRule('platforms', [
@ -326,7 +326,8 @@ class Project extends Model
}
$projectProviders[] = new Document([
'name' => ucfirst($key),
'key' => $key,
'name' => $provider['name'] ?? '',
'appId' => $providerValues[$key . 'Appid'] ?? '',
'secret' => $providerValues[$key . 'Secret'] ?? '',
'enabled' => $providerValues[$key . 'Enabled'] ?? false,

View file

@ -15,6 +15,12 @@ class Provider extends Model
public function __construct()
{
$this
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Provider.',
'default' => '',
'example' => 'github',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Provider name.',

View file

@ -1316,7 +1316,7 @@ trait AccountBase
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::formatTz($response['body']['expire'])), 0);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$this->assertNotFalse($expireTime);

View file

@ -839,7 +839,7 @@ class ProjectsConsoleClientTest extends Scope
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
if ($responseProvider['name'] === ucfirst($key)) {
if ($responseProvider['key'] === $key) {
$this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']);
$this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']);
$this->assertFalse($responseProvider['enabled']);
@ -881,7 +881,7 @@ class ProjectsConsoleClientTest extends Scope
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
if ($responseProvider['name'] === ucfirst($key)) {
if ($responseProvider['key'] === $key) {
// On first provider, test enabled=false
$this->assertEquals($i !== 0, $responseProvider['enabled']);
$asserted = true;