remove legacy hackathon code

This commit is contained in:
Atharva Deosthale
2025-11-21 16:28:13 +05:30
parent 86878e3b55
commit 92f9ddfc33
16 changed files with 102 additions and 4580 deletions
+1 -2
View File
@@ -5,7 +5,6 @@ $common = include __DIR__ . '/collections/common.php';
$projects = include __DIR__ . '/collections/projects.php';
$databases = include __DIR__ . '/collections/databases.php';
$platform = include __DIR__ . '/collections/platform.php';
$authPlans = include __DIR__ . '/collections/auth_plans.php';
$logs = include __DIR__ . '/collections/logs.php';
// see - http.php#245
@@ -28,7 +27,7 @@ $collections = [
'buckets' => $buckets,
'databases' => $databases,
'projects' => array_merge($projects, $common),
'console' => array_merge($platform, $authPlans, $common),
'console' => array_merge($platform, $common),
'logs' => $logs,
];
-676
View File
@@ -1,676 +0,0 @@
<?php
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
return [
'auth_plans' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('auth_plans'),
'name' => 'Auth Plans',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('planId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripePriceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripeMeterId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripeProductId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('price'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('currency'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 3,
'signed' => true,
'required' => true,
'default' => 'usd',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('interval'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 10,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('features'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('isDefault'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('isFree'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('maxUsers'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('active'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => true,
'array' => false,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_planId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['projectId', 'planId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_default'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId', 'isDefault'],
'lengths' => [Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_active'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId', 'active'],
'lengths' => [Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_fulltext_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
'auth_features' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('auth_features'),
'name' => 'Auth Features',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('featureId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('active'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => true,
'array' => false,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_featureId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['projectId', 'featureId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_fulltext_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
'auth_plan_features' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('auth_plan_features'),
'name' => 'Auth Plan Features',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('planId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('featureId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('currency'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 3,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('interval'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 10,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('includedUnits'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('tiersMode'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => false,
'default' => 'graduated',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('tiers'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('usageCap'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripePriceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripeMeterId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('active'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => true,
'array' => false,
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_plan'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId', 'planId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_plan_feature'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['projectId', 'planId', 'featureId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_active'),
'type' => Database::INDEX_KEY,
'attributes' => ['active'],
'lengths' => [0],
'orders' => [Database::ORDER_ASC],
],
],
],
'auth_usage_events' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('auth_usage_events'),
'name' => 'Auth Usage Events',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('planId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('featureId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('value'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('identifier'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('timestamp'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_user_feature_time'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId', 'userId', 'featureId', 'timestamp'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY, Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_plan_feature_time'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId', 'planId', 'featureId', 'timestamp'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY, Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_identifier'),
'type' => Database::INDEX_KEY,
'attributes' => ['identifier'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
],
],
];
-109
View File
@@ -364,94 +364,6 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('planId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripeCustomerId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stripeSubscriptionId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('subscriptionStatus'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20,
'signed' => true,
'required' => false,
'default' => 'none',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('subscriptionCurrentPeriodStart'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('subscriptionCurrentPeriodEnd'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('subscriptionCancelAtPeriodEnd'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('subscriptionTrialEnd'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
@@ -524,27 +436,6 @@ return [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_planId'),
'type' => Database::INDEX_KEY,
'attributes' => ['planId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_stripeCustomerId'),
'type' => Database::INDEX_KEY,
'attributes' => ['stripeCustomerId'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_subscriptionStatus'),
'type' => Database::INDEX_KEY,
'attributes' => ['subscriptionStatus'],
'lengths' => [20],
'orders' => [Database::ORDER_ASC],
],
],
],
-66
View File
@@ -342,72 +342,6 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('authSubscriptionsEnabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('authStripeSecretKey'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => ID::custom('authStripePublishableKey'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('authStripeWebhookSecret'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => ID::custom('authStripeWebhookEndpointId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('authStripeCurrency'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 3,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
-13
View File
@@ -274,19 +274,6 @@ return [
'optional' => false,
'icon' => '',
],
'authPlans' => [
'key' => 'authPlans',
'name' => 'Auth Plans',
'subtitle' => 'The Auth Plans service allows you to manage subscription plans for authentication.',
'description' => '',
'controller' => 'api/auth-plans.php',
'sdk' => false,
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => true,
'icon' => '',
],
'migrations' => [
'key' => 'migrations',
'name' => 'Migrations',
-492
View File
@@ -5183,495 +5183,3 @@ App::delete('/v1/account/identities/:identityId')
return $response->noContent();
});
App::get('/v1/account/subscription')
->desc('Get account subscription')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'subscription',
name: 'getSubscription',
description: '/docs/references/account/get-subscription.md',
auth: [AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (Response $response, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
$planId = (string) $user->getAttribute('planId', '');
$plan = null;
if ($planId !== '') {
$candidate = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('active', [true])
]));
$plan = ($candidate instanceof \Utopia\Database\Document && !$candidate->isEmpty()) ? $candidate : null;
}
if (!$plan) {
$candidate = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('isDefault', [true]),
Query::equal('active', [true])
]));
$plan = ($candidate instanceof \Utopia\Database\Document && !$candidate->isEmpty()) ? $candidate : null;
}
// Final safeguard: return default even if not explicitly marked active
if (!$plan) {
$candidate = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('isDefault', [true])
]));
$plan = ($candidate instanceof \Utopia\Database\Document && !$candidate->isEmpty()) ? $candidate : null;
}
if ($plan) {
$planId = (string) $plan->getAttribute('planId');
}
$features = [];
if ($planId !== '') {
$assigned = Authorization::skip(fn () => $dbForPlatform->find('auth_plan_features', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('active', [true])
]));
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$dbForPlatform
);
$customerId = (string) $user->getAttribute('stripeCustomerId', '');
$periodStart = $user->getAttribute('subscriptionCurrentPeriodStart');
$periodEnd = $user->getAttribute('subscriptionCurrentPeriodEnd');
$startTs = $periodStart ? strtotime($periodStart) : 0;
$endTs = $periodEnd ? strtotime($periodEnd) : time();
foreach ($assigned as $af) {
$featureId = (string) $af->getAttribute('featureId');
$usage = 0;
if ((string)$af->getAttribute('type') === 'metered') {
$featureStartTs = $startTs;
$featureEndTs = $endTs;
if ($featureStartTs <= 0 || $featureEndTs <= 0) {
$interval = (string) $af->getAttribute('interval', 'month');
$seconds = match ($interval) {
'day' => 86400,
'week' => 86400 * 7,
'year' => 86400 * 365,
default => 86400 * 30,
};
$featureEndTs = time();
$featureStartTs = $featureEndTs - $seconds;
}
try {
$usage = $stripeService->getFeatureUsageTotalLocal($user->getId(), $planId, $featureId, $featureStartTs, $featureEndTs);
} catch (\Throwable $_) {
}
}
$features[] = [
'featureId' => $featureId,
'type' => (string) $af->getAttribute('type'),
'enabled' => (bool) $af->getAttribute('enabled', true),
'currency' => $af->getAttribute('currency'),
'interval' => $af->getAttribute('interval'),
'includedUnits' => (int) $af->getAttribute('includedUnits', 0),
'tiersMode' => $af->getAttribute('tiersMode'),
'tiers' => (array) $af->getAttribute('tiers', []),
'usage' => $usage,
'usageCap' => $af->getAttribute('usageCap'),
];
}
}
$response->dynamic(new Document([
'planId' => $planId !== '' ? $planId : null,
'planName' => $plan ? $plan->getAttribute('name') : null,
'status' => $user->getAttribute('subscriptionStatus', 'none'),
'currentPeriodStart' => $user->getAttribute('subscriptionCurrentPeriodStart'),
'currentPeriodEnd' => $user->getAttribute('subscriptionCurrentPeriodEnd'),
'cancelAtPeriodEnd' => $user->getAttribute('subscriptionCancelAtPeriodEnd', false),
'trialEnd' => $user->getAttribute('subscriptionTrialEnd'),
'features' => $features
]), Response::MODEL_ANY);
});
App::post('/v1/account/subscription/checkout')
->desc('Create checkout session')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'subscription',
name: 'createSubscriptionCheckout',
description: '/docs/references/account/create-checkout.md',
auth: [AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('planId', '', new Text(128), 'Plan ID to subscribe to.')
->param('successUrl', '', new URL(), 'URL to redirect on success.')
->param('cancelUrl', '', new URL(), 'URL to redirect on cancel.')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (
string $planId,
string $successUrl,
string $cancelUrl,
Response $response,
Document $user,
Document $project,
Database $dbForProject,
Database $dbForPlatform
) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
$plan = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('active', [true])
]));
if (!$plan) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Plan not found');
}
if ($plan->getAttribute('isFree')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot checkout for free plan');
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$dbForPlatform
);
try {
$additionalItems = $stripeService->buildAdditionalLineItemsForPlan($planId);
$session = $stripeService->createCheckoutSession(
$user,
$plan->getAttribute('stripePriceId'),
$successUrl,
$cancelUrl,
(string) $plan->getAttribute('planId'),
$additionalItems
);
$response->dynamic(new Document([
'checkoutUrl' => $session['url']
]), Response::MODEL_ANY);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::post('/v1/account/subscription/portal')
->desc('Create customer portal session')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'subscription',
name: 'createSubscriptionPortal',
description: '/docs/references/account/create-portal.md',
auth: [AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('returnUrl', '', new URL(), 'URL to return to.')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->action(function (
string $returnUrl,
Response $response,
Document $user,
Document $project,
Database $dbForProject
) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
$customerId = $user->getAttribute('stripeCustomerId');
if (!$customerId) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'No active subscription found');
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
try {
$session = $stripeService->createPortalSession($customerId, $returnUrl);
$response->dynamic(new Document([
'portalUrl' => $session['url']
]), Response::MODEL_ANY);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::put('/v1/account/subscription')
->desc('Update subscription')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'subscription',
name: 'updateSubscription',
description: '/docs/references/account/update-subscription.md',
auth: [AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('planId', '', new Text(128), 'New plan ID.')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (
string $planId,
Response $response,
Document $user,
Document $project,
Database $dbForProject,
Database $dbForPlatform
) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
$subscriptionId = $user->getAttribute('stripeSubscriptionId');
if (!$subscriptionId) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'No active subscription found');
}
$plan = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('active', [true])
]));
if (!$plan) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Plan not found');
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
try {
$subscription = $stripeService->updateSubscription(
$subscriptionId,
$plan->getAttribute('stripePriceId')
);
$stripeService->syncSubscriptionStatus($user->getId(), $subscription);
$response->dynamic(new Document([
'success' => true
]), Response::MODEL_ANY);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::delete('/v1/account/subscription')
->desc('Cancel subscription')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'subscription',
name: 'cancelSubscription',
description: '/docs/references/account/cancel-subscription.md',
auth: [AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('atPeriodEnd', true, new Boolean(), 'Cancel at period end.', true)
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->action(function (
bool $atPeriodEnd,
Response $response,
Document $user,
Document $project,
Database $dbForProject
) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
$subscriptionId = $user->getAttribute('stripeSubscriptionId');
if (!$subscriptionId) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'No active subscription found');
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
try {
$subscription = $stripeService->cancelSubscription($subscriptionId, $atPeriodEnd);
if ($atPeriodEnd) {
$stripeService->syncSubscriptionStatus($user->getId(), $subscription);
}
$response->dynamic(new Document([
'success' => true,
'cancelAtPeriodEnd' => $atPeriodEnd
]), Response::MODEL_ANY);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::post('/v1/usage/ingest')
->desc('Ingest metered usage for a feature')
->groups(['api'])
->label('scope', 'account')
->label('sdk', new Method(
namespace: 'account',
group: 'usage',
name: 'ingestUsage',
description: '/docs/references/account/ingest-usage.md',
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('planId', '', new Text(128), 'Plan ID to attribute usage to.')
->param('featureId', '', new Text(128), 'Feature ID to attribute usage to.')
->param('value', 1, new Integer(), 'Usage value to ingest.')
->param('userId', '', new UID(), 'User ID to attribute usage to. Ignored for client SDK calls.', true)
->param('timestamp', null, new Integer(true), 'Unix timestamp for the event.', true)
->param('identifier', null, new Text(256), 'Idempotency identifier for this event.', true)
->inject('response')
->inject('project')
->inject('user')
->inject('apiKey')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (
string $planId,
string $featureId,
int $value,
string $userId,
?int $timestamp,
?string $identifier,
Response $response,
Document $project,
Document $user,
?Key $apiKey,
Database $dbForProject,
Database $dbForPlatform
) {
if (!$project->getAttribute('authSubscriptionsEnabled')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Subscriptions not enabled for this project');
}
// Determine effective userId
$effectiveUserId = '';
if (!empty($apiKey)) {
// Admin/API key: honor provided userId
if (empty($userId)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'userId is required when using API key');
}
$effectiveUserId = $userId;
} else {
// Client SDK (session/JWT): ignore provided userId, use logged-in user
if ($user->isEmpty()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$effectiveUserId = $user->getId();
}
// Validate plan & feature assignment exists and is metered
$assignment = Authorization::skip(fn () => $dbForPlatform->findOne('auth_plan_features', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('featureId', [$featureId]),
Query::equal('active', [true])
]));
if (!$assignment || $assignment->isEmpty()) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Feature not assigned to plan');
}
if ((string)$assignment->getAttribute('type') !== 'metered') {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Feature is not metered');
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$dbForPlatform
);
try {
$result = $stripeService->ingestUsage($value, $effectiveUserId, $planId, $featureId, $timestamp, $identifier);
$response->dynamic(new Document([
'success' => true,
'id' => $result['id'] ?? null
]), Response::MODEL_ANY);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
File diff suppressed because it is too large Load Diff
+41 -407
View File
@@ -2,10 +2,7 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Subscription\Exception\SubscriptionException;
use Appwrite\Auth\Subscription\StripeService;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Auth\Validator\StripeKey;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
use Appwrite\Event\Validator\Event;
@@ -49,6 +46,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -63,209 +61,6 @@ App::init()
}
});
App::post('/v1/projects/:projectId/auth/features')
->desc('Create auth feature')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'authFeatures',
name: 'createAuthFeature',
description: '/docs/references/projects/create-auth-feature.md',
auth: [AuthType::ADMIN, AuthType::SESSION],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_ANY,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('featureId', '', new Text(128), 'Feature unique ID.')
->param('name', '', new Text(128), 'Feature name.')
->param('type', '', new WhiteList(['boolean', 'metered']), 'Feature type: boolean or metered')
->param('description', '', new Text(256), 'Feature description.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (
string $projectId,
string $featureId,
string $name,
string $type,
string $description,
Response $response,
Database $dbForPlatform
) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$existing = $dbForPlatform->findOne('auth_features', [
Query::equal('projectId', [$projectId]),
Query::equal('featureId', [$featureId])
]);
if ($existing instanceof Document && !$existing->isEmpty()) {
throw new Exception(Exception::RESOURCE_ALREADY_EXISTS, 'Feature already exists');
}
$doc = $dbForPlatform->createDocument('auth_features', new Document([
'$id' => ID::unique(),
'projectInternalId' => $project->getSequence(),
'projectId' => $projectId,
'featureId' => $featureId,
'name' => $name,
'type' => $type,
'description' => $description,
'active' => true,
'search' => implode(' ', [$featureId, $name, $description])
]));
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($doc, Response::MODEL_ANY);
});
App::get('/v1/projects/:projectId/auth/features')
->desc('List auth features')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'authFeatures',
name: 'listAuthFeatures',
description: '/docs/references/projects/list-auth-features.md',
auth: [AuthType::ADMIN, AuthType::SESSION],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DOCUMENT_LIST,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('queries', [], new ArrayList(new Text(256)), 'Array of query strings.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, array $queries, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$queries[] = Query::equal('projectId', [$projectId]);
$queries[] = Query::equal('active', [true]);
$docs = $dbForPlatform->find('auth_features', $queries);
$response->dynamic(new Document([
'total' => count($docs),
'documents' => $docs
]), Response::MODEL_DOCUMENT_LIST);
});
App::put('/v1/projects/:projectId/auth/features/:featureId')
->desc('Update auth feature')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'authFeatures',
name: 'updateAuthFeature',
description: '/docs/references/projects/update-auth-feature.md',
auth: [AuthType::ADMIN, AuthType::SESSION],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_ANY,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('featureId', '', new Text(128), 'Feature unique ID.')
->param('name', null, new Text(128), 'Feature name.', true)
->param('type', null, new WhiteList(['boolean', 'metered'], true), 'Feature type.', true)
->param('description', null, new Text(256), 'Feature description.', true)
->param('active', null, new Boolean(), 'Active state.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (
string $projectId,
string $featureId,
?string $name,
?string $type,
?string $description,
?bool $active,
Response $response,
Database $dbForPlatform
) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$doc = $dbForPlatform->findOne('auth_features', [
Query::equal('projectId', [$projectId]),
Query::equal('featureId', [$featureId])
]);
if (!$doc) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Feature not found');
}
if ($name !== null) {
$doc->setAttribute('name', $name);
}
if ($type !== null) {
$doc->setAttribute('type', $type);
}
if ($description !== null) {
$doc->setAttribute('description', $description);
}
if ($active !== null) {
$doc->setAttribute('active', $active);
}
$doc->setAttribute('search', implode(' ', [
$doc->getAttribute('featureId'),
$doc->getAttribute('name'),
$doc->getAttribute('description')
]));
$doc = $dbForPlatform->updateDocument('auth_features', $doc->getId(), $doc);
$response->dynamic($doc, Response::MODEL_ANY);
});
App::delete('/v1/projects/:projectId/auth/features/:featureId')
->desc('Delete auth feature')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'authFeatures',
name: 'deleteAuthFeature',
description: '/docs/references/projects/delete-auth-feature.md',
auth: [AuthType::ADMIN, AuthType::SESSION],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('featureId', '', new Text(128), 'Feature unique ID.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $featureId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$doc = $dbForPlatform->findOne('auth_features', [
Query::equal('projectId', [$projectId]),
Query::equal('featureId', [$featureId])
]);
if (!$doc) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Feature not found');
}
$dbForPlatform->deleteDocument('auth_features', $doc->getId());
$response->noContent();
});
App::post('/v1/projects')
->desc('Create project')
->groups(['api', 'projects'])
@@ -527,8 +322,7 @@ App::get('/v1/projects/:projectId')
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $projectId, Response $response, Database $dbForPlatform, Database $dbForProject) {
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -885,9 +679,9 @@ App::patch('/v1/projects/:projectId/oauth2')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
->param('appId', null, new Nullable(new Text(256)), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new text(512)), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Provider status. Set to \'false\' to disable new session creation.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForPlatform) {
@@ -1441,10 +1235,10 @@ App::get('/v1/projects/:projectId/webhooks')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $projectId, Response $response, Database $dbForPlatform, Database $dbForProject) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1459,7 +1253,7 @@ App::get('/v1/projects/:projectId/webhooks')
$response->dynamic(new Document([
'webhooks' => $webhooks,
'total' => count($webhooks),
'total' => $includeTotal ? count($webhooks) : 0,
]), Response::MODEL_WEBHOOK_LIST);
});
@@ -1683,8 +1477,8 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1739,9 +1533,10 @@ App::get('/v1/projects/:projectId/keys')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1756,7 +1551,7 @@ App::get('/v1/projects/:projectId/keys')
$response->dynamic(new Document([
'keys' => $keys,
'total' => count($keys),
'total' => $includeTotal ? count($keys) : 0,
]), Response::MODEL_KEY_LIST);
});
@@ -1821,8 +1616,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1964,7 +1759,28 @@ App::post('/v1/projects/:projectId/platforms')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', null, new WhiteList([Platform::TYPE_WEB, Platform::TYPE_FLUTTER_WEB, Platform::TYPE_FLUTTER_IOS, Platform::TYPE_FLUTTER_ANDROID, Platform::TYPE_FLUTTER_LINUX, Platform::TYPE_FLUTTER_MACOS, Platform::TYPE_FLUTTER_WINDOWS, Platform::TYPE_APPLE_IOS, Platform::TYPE_APPLE_MACOS, Platform::TYPE_APPLE_WATCHOS, Platform::TYPE_APPLE_TVOS, Platform::TYPE_ANDROID, Platform::TYPE_UNITY, Platform::TYPE_REACT_NATIVE_IOS, Platform::TYPE_REACT_NATIVE_ANDROID], true), 'Platform type.')
->param(
'type',
null,
new WhiteList([
Platform::TYPE_WEB,
Platform::TYPE_FLUTTER_WEB,
Platform::TYPE_FLUTTER_IOS,
Platform::TYPE_FLUTTER_ANDROID,
Platform::TYPE_FLUTTER_LINUX,
Platform::TYPE_FLUTTER_MACOS,
Platform::TYPE_FLUTTER_WINDOWS,
Platform::TYPE_APPLE_IOS,
Platform::TYPE_APPLE_MACOS,
Platform::TYPE_APPLE_WATCHOS,
Platform::TYPE_APPLE_TVOS,
Platform::TYPE_ANDROID,
Platform::TYPE_UNITY,
Platform::TYPE_REACT_NATIVE_IOS,
Platform::TYPE_REACT_NATIVE_ANDROID,
], true),
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
)
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
@@ -2021,9 +1837,10 @@ App::get('/v1/projects/:projectId/platforms')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -2038,7 +1855,7 @@ App::get('/v1/projects/:projectId/platforms')
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => count($platforms),
'total' => $includeTotal ? count($platforms) : 0,
]), Response::MODEL_PLATFORM_LIST);
});
@@ -2845,187 +2662,4 @@ App::patch('/v1/projects/:projectId/auth/session-invalidation')
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::put('/v1/projects/:projectId/auth/subscriptions')
->desc('Configure auth subscriptions')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'configureSubscriptions',
description: '/docs/references/projects/configure-subscriptions.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('stripeSecretKey', '', new StripeKey(), 'Stripe secret key.')
->inject('response')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $projectId, string $stripeSecretKey, Response $response, Database $dbForPlatform, Database $dbForProject) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
try {
$stripeService = new StripeService($stripeSecretKey, $dbForProject, $project);
$config = $stripeService->initializeAccount();
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('authSubscriptionsEnabled', true)
->setAttribute('authStripeSecretKey', $stripeSecretKey)
->setAttribute('authStripePublishableKey', $config['publishableKey'])
->setAttribute('authStripeWebhookSecret', $config['webhookSecret'])
->setAttribute('authStripeWebhookEndpointId', $config['webhookEndpointId'])
->setAttribute('authStripeCurrency', $config['currency']));
// Ensure a default free plan exists
$existingDefault = $dbForPlatform->findOne('auth_plans', [
\Utopia\Database\Query::equal('projectId', [$projectId]),
\Utopia\Database\Query::equal('isDefault', [true])
]);
if (!($existingDefault instanceof \Utopia\Database\Document) || $existingDefault->isEmpty()) {
$dbForPlatform->createDocument('auth_plans', new \Utopia\Database\Document([
'$id' => \Utopia\Database\Helpers\ID::unique(),
'projectInternalId' => $project->getSequence(),
'projectId' => $projectId,
'planId' => 'free',
'name' => 'Free',
'description' => 'Default free plan',
'stripeProductId' => null,
'stripePriceId' => null,
'price' => 0,
'currency' => $config['currency'],
'interval' => null,
'features' => [],
'isDefault' => true,
'isFree' => true,
'maxUsers' => null,
'active' => true,
'search' => 'free Default free plan'
]));
}
$response->dynamic($project, Response::MODEL_PROJECT);
} catch (SubscriptionException $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
App::get('/v1/projects/:projectId/auth/subscriptions')
->desc('Get auth subscription configuration')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'getSubscriptionsConfig',
description: '/docs/references/projects/get-subscriptions-config.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$response->dynamic(new Document([
'enabled' => $project->getAttribute('authSubscriptionsEnabled', false),
'publishableKey' => $project->getAttribute('authStripePublishableKey'),
'currency' => $project->getAttribute('authStripeCurrency'),
]), Response::MODEL_ANY);
});
App::delete('/v1/projects/:projectId/auth/subscriptions')
->desc('Remove auth subscription configuration')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'removeSubscriptionsConfig',
description: '/docs/references/projects/remove-subscriptions-config.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $projectId, Response $response, Database $dbForPlatform, Database $dbForProject) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($project->getAttribute('authSubscriptionsEnabled')) {
try {
$stripe = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$dbForPlatform
);
$endpointId = $project->getAttribute('authStripeWebhookEndpointId');
if (!empty($endpointId)) {
$stripe->deleteWebhookEndpoint($endpointId);
}
$plans = $dbForPlatform->find('auth_plans', [
\Utopia\Database\Query::equal('projectId', [$projectId])
]);
foreach ($plans as $plan) {
$priceId = $plan->getAttribute('stripePriceId');
$productId = $plan->getAttribute('stripeProductId');
try {
if ($priceId) {
$stripe->deactivatePrice($priceId);
}
} catch (\Throwable $_) {
}
try {
if ($productId) {
$stripe->deactivateProduct($productId);
}
} catch (\Throwable $_) {
}
$dbForPlatform->deleteDocument('auth_plans', $plan->getId());
}
} catch (\Throwable $_) {
}
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('authSubscriptionsEnabled', false)
->setAttribute('authStripeSecretKey', null)
->setAttribute('authStripePublishableKey', null)
->setAttribute('authStripeWebhookSecret', null)
->setAttribute('authStripeWebhookEndpointId', null)
->setAttribute('authStripeCurrency', null));
$response->dynamic($project, Response::MODEL_PROJECT);
});
});
+58 -247
View File
@@ -16,7 +16,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -49,12 +49,14 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -97,6 +99,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = new Document([
'$id' => $userId,
@@ -124,6 +132,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash === 'plaintext') {
@@ -208,8 +221,8 @@ App::post('/v1/users')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('email', null, new Nullable(new EmailValidator()), 'User email.', true)
->param('phone', null, new Nullable(new Phone()), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -243,7 +256,7 @@ App::post('/v1/users/bcrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -278,7 +291,7 @@ App::post('/v1/users/md5')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -313,7 +326,7 @@ App::post('/v1/users/argon2')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -348,7 +361,7 @@ App::post('/v1/users/sha')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -390,7 +403,7 @@ App::post('/v1/users/phpass')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -425,7 +438,7 @@ App::post('/v1/users/scrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.')
@@ -473,7 +486,7 @@ App::post('/v1/users/scrypt-modified')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
@@ -527,7 +540,7 @@ App::post('/v1/users/:userId/targets')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -605,11 +618,10 @@ App::get('/v1/users')
))
->param('queries', [], new Users(), '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(', ', Users::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->action(function (array $queries, string $search, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
@@ -649,10 +661,10 @@ App::get('/v1/users')
$users = [];
$total = 0;
$dbForProject->skipFilters(function () use ($dbForProject, $queries, &$users, &$total) {
$dbForProject->skipFilters(function () use ($dbForProject, $queries, $includeTotal, &$users, &$total) {
try {
$users = $dbForProject->find('users', $queries);
$total = $dbForProject->count('users', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('users', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
} catch (QueryException $e) {
@@ -660,49 +672,6 @@ App::get('/v1/users')
}
}, ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships']);
$planIds = [];
foreach ($users as $u) {
$pid = (string) $u->getAttribute('planId', '');
if ($pid !== '') {
$planIds[$pid] = true;
}
}
$planMap = [];
if (!empty($planIds)) {
$plans = $dbForPlatform->find('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', array_keys($planIds)),
Query::equal('active', [true])
]);
foreach ($plans as $p) {
$planMap[(string) $p->getAttribute('planId')] = $p;
}
}
$defaultPlan = $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('isDefault', [true]),
Query::equal('active', [true])
]);
foreach ($users as $u) {
$pid = (string) $u->getAttribute('planId', '');
$p = null;
if ($pid !== '' && isset($planMap[$pid])) {
$p = $planMap[$pid];
} elseif ($defaultPlan) {
$p = $defaultPlan;
}
if ($p) {
$u->setAttribute('plan', [
'id' => $p->getAttribute('planId'),
'name' => $p->getAttribute('name'),
'price' => $p->getAttribute('price'),
'currency' => $p->getAttribute('currency'),
'interval' => $p->getAttribute('interval'),
'isFree' => $p->getAttribute('isFree', false)
]);
}
}
$response->dynamic(new Document([
'users' => $users,
'total' => $total,
@@ -729,9 +698,7 @@ App::get('/v1/users/:userId')
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->action(function (string $userId, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project) {
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
@@ -739,177 +706,6 @@ App::get('/v1/users/:userId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$pid = (string) $user->getAttribute('planId', '');
$plan = null;
if ($pid !== '') {
$candidate = $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$pid]),
Query::equal('active', [true])
]);
$plan = ($candidate instanceof \Utopia\Database\Document && !$candidate->isEmpty()) ? $candidate : null;
} else {
$candidate = $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('isDefault', [true]),
Query::equal('active', [true])
]);
$plan = ($candidate instanceof \Utopia\Database\Document && !$candidate->isEmpty()) ? $candidate : null;
}
if ($plan instanceof \Utopia\Database\Document && !$plan->isEmpty()) {
$features = [];
$resolvedPlanId = (string) $plan->getAttribute('planId');
$assigned = [];
if ($resolvedPlanId !== '') {
$assigned = Authorization::skip(fn () => $dbForPlatform->find('auth_plan_features', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$resolvedPlanId]),
Query::equal('active', [true])
]));
}
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$dbForPlatform
);
$customerId = (string) $user->getAttribute('stripeCustomerId', '');
$periodStart = $user->getAttribute('subscriptionCurrentPeriodStart');
$periodEnd = $user->getAttribute('subscriptionCurrentPeriodEnd');
$startTs = $periodStart ? strtotime($periodStart) : 0;
$endTs = $periodEnd ? strtotime($periodEnd) : time();
foreach ($assigned as $af) {
$featureId = (string) $af->getAttribute('featureId');
$usage = 0;
if ((string)$af->getAttribute('type') === 'metered') {
try {
$usage = $stripeService->getFeatureUsageTotalLocal($user->getId(), $resolvedPlanId, $featureId, $startTs, $endTs);
} catch (\Throwable $_) {
}
}
$features[] = [
'featureId' => (string) $af->getAttribute('featureId'),
'type' => (string) $af->getAttribute('type'),
'enabled' => (bool) $af->getAttribute('enabled', true),
'currency' => $af->getAttribute('currency'),
'interval' => $af->getAttribute('interval'),
'includedUnits' => (int) $af->getAttribute('includedUnits', 0),
'tiersMode' => $af->getAttribute('tiersMode'),
'tiers' => (array) $af->getAttribute('tiers', []),
'usage' => $usage,
'usageCap' => $af->getAttribute('usageCap'),
];
}
$user->setAttribute('plan', [
'id' => $plan->getAttribute('planId'),
'name' => $plan->getAttribute('name'),
'price' => $plan->getAttribute('price'),
'currency' => $plan->getAttribute('currency'),
'interval' => $plan->getAttribute('interval'),
'isFree' => $plan->getAttribute('isFree', false),
'features' => $features
]);
}
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/:userId/plan')
->desc('Assign plan to user (admin)')
->groups(['api', 'users'])
->label('scope', 'users.write')
->label('sdk', new Method(
namespace: 'users',
group: 'users',
name: 'assignPlan',
description: '/docs/references/users/assign-plan.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USER,
)
]
))
->param('userId', '', new UID(), 'User ID.')
->param('planId', '', new Text(128), 'Plan ID to assign.')
->param('complimentary', true, new Boolean(), 'If true, create a complimentary Stripe subscription for one billing interval.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (string $userId, string $planId, bool $complimentary, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$plan = $dbForPlatform->findOne('auth_plans', [
Query::equal('projectId', [$project->getId()]),
Query::equal('planId', [$planId]),
Query::equal('active', [true])
]);
if (!$plan) {
throw new Exception(Exception::GENERAL_NOT_FOUND, 'Plan not found');
}
$user->setAttribute('planId', $planId);
$now = time();
$interval = (string) $plan->getAttribute('interval', 'month');
$seconds = match ($interval) {
'day' => 86400,
'week' => 86400 * 7,
'year' => 86400 * 365,
default => 86400 * 30,
};
$user->setAttribute('subscriptionStatus', 'active');
$user->setAttribute('subscriptionCurrentPeriodStart', DateTime::format(new \DateTime('@' . $now)));
$user->setAttribute('subscriptionCurrentPeriodEnd', DateTime::format(new \DateTime('@' . ($now + $seconds))));
$user->setAttribute('subscriptionCancelAtPeriodEnd', true);
$user->setAttribute('subscriptionTrialEnd', DateTime::format(new \DateTime('@' . ($now + $seconds))));
if ($plan->getAttribute('isFree', false) && $project->getAttribute('authSubscriptionsEnabled')) {
$stripe = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
$subscriptionId = (string) $user->getAttribute('stripeSubscriptionId', '');
if ($subscriptionId === '') {
$customerId = (string) $user->getAttribute('stripeCustomerId', '');
if ($customerId !== '') {
try {
$found = $stripe->findActiveSubscriptionId($customerId);
if (!empty($found)) {
$subscriptionId = (string) $found;
}
} catch (\Throwable $_) {
}
}
}
if ($subscriptionId !== '') {
try {
$stripe->cancelSubscription($subscriptionId, false);
} catch (\Throwable $_) {
}
$user->setAttribute('stripeSubscriptionId', null);
}
} elseif ($complimentary && !$plan->getAttribute('isFree', false) && $project->getAttribute('authSubscriptionsEnabled')) {
$stripe = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
$customerId = (string) $user->getAttribute('stripeCustomerId', '');
if ($customerId === '') {
$customerId = $stripe->ensureCustomer($user);
$user->setAttribute('stripeCustomerId', $customerId);
}
$stripe->createComplimentarySubscription($customerId, (string) $plan->getAttribute('stripePriceId'), $interval, $user->getId());
}
$user = $dbForProject->updateDocument('users', $userId, $user);
$response->dynamic($user, Response::MODEL_USER);
});
@@ -1002,10 +798,11 @@ App::get('/v1/users/:userId/sessions')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->action(function (string $userId, Response $response, Database $dbForProject, Locale $locale) {
->action(function (string $userId, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1027,7 +824,7 @@ App::get('/v1/users/:userId/sessions')
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
'total' => $includeTotal ? count($sessions) : 0,
]), Response::MODEL_SESSION_LIST);
});
@@ -1051,9 +848,10 @@ App::get('/v1/users/:userId/memberships')
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (string $userId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1087,7 +885,7 @@ App::get('/v1/users/:userId/memberships')
$response->dynamic(new Document([
'memberships' => $memberships,
'total' => count($memberships),
'total' => $includeTotal ? count($memberships) : 0,
]), Response::MODEL_MEMBERSHIP_LIST);
});
@@ -1110,11 +908,12 @@ App::get('/v1/users/:userId/logs')
))
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1183,7 +982,7 @@ App::get('/v1/users/:userId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getSequence(), $queries),
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence(), $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1207,9 +1006,10 @@ App::get('/v1/users/:userId/targets')
))
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Targets(), '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(', ', Targets::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject) {
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -1249,7 +1049,7 @@ App::get('/v1/users/:userId/targets')
}
try {
$targets = $dbForProject->find('targets', $queries);
$total = $dbForProject->count('targets', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('targets', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -1278,9 +1078,10 @@ App::get('/v1/users/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)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
@@ -1317,10 +1118,9 @@ App::get('/v1/users/identities')
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$identities = $dbForProject->find('identities', $queries);
$total = $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('identities', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -1615,7 +1415,7 @@ App::patch('/v1/users/:userId/email')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('email', '', new Email(allowEmpty: true), 'User email.')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -1650,9 +1450,20 @@ App::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
try {
@@ -1913,7 +1724,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -2898,4 +2709,4 @@ App::get('/v1/users/usage')
'users' => $usage[$metrics[0]]['data'],
'sessions' => $usage[$metrics[1]]['data'],
]), Response::MODEL_USAGE_USERS);
});
});
+2 -60
View File
@@ -41,7 +41,6 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Locale\Locale;
@@ -192,7 +191,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$resourceId = $rule->getAttribute('deploymentResourceId', '');
$type = ($resourceType === 'site') ? 'sites' : 'functions';
$exception = new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, view: $errorView);
$exception->addCTA('View deployments', $url . '/console/project-' . $project->getAttribute('region', 'default') . '-' . $project->getId() . '/' . $type . '/' . $resourceType . '-' . $resourceId);
$exception->addCTA('View deployments', $url . '/console/project-' . $project->getAttribute('region', 'default') . '-' . $projectId . '/' . $type . '/' . $resourceType . '-' . $resourceId);
throw $exception;
}
@@ -1640,61 +1639,4 @@ if (!empty(Method::getErrors())) {
// Modules
$platform = new Appwrite();
$platform->init(Service::TYPE_HTTP);
App::post('/v1/webhooks/stripe/auth/:projectId')
->desc('Handle Stripe webhook for auth subscriptions')
->groups(['webhooks'])
->label('scope', 'public')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('getProjectDB')
->param('projectId', '', new UID(), 'Project unique ID.')
->action(function (string $projectId, $request, $response, $dbForPlatform, callable $getProjectDB) {
$payload = $request->getRawPayload();
$signature = $request->getHeader('stripe-signature') ?: $request->getHeader('Stripe-Signature') ?: ($_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '');
if (empty($signature)) {
throw new AppwriteException(AppwriteException::GENERAL_BAD_REQUEST, 'Missing Stripe signature');
}
$event = json_decode($payload, true);
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if ($project->isEmpty()) {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
$dbForProject = $getProjectDB($project);
$webhookSecret = $project->getAttribute('authStripeWebhookSecret');
if (empty($webhookSecret)) {
throw new AppwriteException(AppwriteException::GENERAL_BAD_REQUEST, 'Webhook not configured');
}
$tmpService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project
);
if (!$tmpService->verifyWebhookSignature($payload, $signature, $webhookSecret)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Invalid webhook signature');
}
$platformDb = $dbForPlatform;
$stripeService = new \Appwrite\Auth\Subscription\StripeService(
$project->getAttribute('authStripeSecretKey'),
$dbForProject,
$project,
$platformDb
);
try {
$stripeService->handleWebhook($event ?? []);
$response->json(['success' => true]);
} catch (\Appwrite\Auth\Subscription\Exception\SubscriptionException $e) {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, $e->getMessage());
}
});
$platform->init(Service::TYPE_HTTP);
@@ -1,13 +0,0 @@
<?php
namespace Appwrite\Auth\Subscription\Exception;
use Exception;
class SubscriptionException extends Exception
{
public function __construct(string $message = '', int $code = 0, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
File diff suppressed because it is too large Load Diff
-78
View File
@@ -1,78 +0,0 @@
<?php
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
class PlanData extends Validator
{
/**
* Get Description
*/
public function getDescription(): string
{
return 'Value must be a valid plan data structure';
}
/**
* Is valid
*
* @param mixed $value
*/
public function isValid($value): bool
{
if (!is_array($value)) {
return false;
}
if (!isset($value['name']) || empty($value['name'])) {
return false;
}
if (!isset($value['planId']) || empty($value['planId'])) {
return false;
}
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $value['planId'])) {
return false;
}
if (isset($value['price']) && (!is_int($value['price']) || $value['price'] < 0)) {
return false;
}
if (isset($value['currency']) && !preg_match('/^[a-z]{3}$/', $value['currency'])) {
return false;
}
if (isset($value['interval']) && !in_array($value['interval'], ['month', 'year', 'week', 'day'])) {
return false;
}
if (isset($value['maxUsers']) && (!is_int($value['maxUsers']) || $value['maxUsers'] < 0)) {
return false;
}
if (isset($value['features']) && !is_array($value['features'])) {
return false;
}
return true;
}
/**
* Is array
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}
-58
View File
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
class StripeKey extends Validator
{
/**
* Get Description
*/
public function getDescription(): string
{
return 'Value must be a valid Stripe API key';
}
/**
* Is valid
*
* @param mixed $value
*/
public function isValid($value): bool
{
if (!is_string($value)) {
return false;
}
if (empty($value)) {
return false;
}
if (preg_match('/^sk_(test|live)_[0-9a-zA-Z]{24,}$/', $value)) {
return true;
}
if (preg_match('/^rk_(test|live)_[0-9a-zA-Z]{24,}$/', $value)) {
return true;
}
return false;
}
/**
* Is array
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
class SubscriptionStatus extends Validator
{
private const VALID_STATUSES = [
'none',
'active',
'canceled',
'incomplete',
'incomplete_expired',
'past_due',
'trialing',
'unpaid',
'paused'
];
/**
* Get Description
*/
public function getDescription(): string
{
return 'Value must be a valid subscription status';
}
/**
* Is valid
*
* @param mixed $value
*/
public function isValid($value): bool
{
if (!is_string($value)) {
return false;
}
return in_array($value, self::VALID_STATUSES);
}
/**
* Is array
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
@@ -133,20 +133,6 @@ class User extends Model
'array' => true,
'example' => [],
])
->addRule('plan', [
'type' => self::TYPE_JSON,
'description' => 'User subscription plan details.',
'required' => false,
'default' => new \stdClass(),
'example' => [
'id' => 'basic',
'name' => 'Basic',
'price' => 999,
'currency' => 'usd',
'interval' => 'month',
'isFree' => false
],
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCESS / 60 / 60 . ' hours.',