Merge branch '1.8.x' into ser-409

This commit is contained in:
Hemachandar 2025-12-05 12:15:08 +05:30
commit 4ebc67c235
2034 changed files with 85462 additions and 12671 deletions

1
.env
View file

@ -103,6 +103,7 @@ _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4

2
.gitattributes vendored
View file

@ -5,3 +5,5 @@ src/** linguist-detectable=false
tests/** linguist-detectable=false
public/scripts/** linguist-detectable=false
public/dist/scripts/** linguist-detectable=false
.github/workflows/*.lock.yml linguist-generated=true merge=ours

83
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,83 @@
# Fixes and upgrades for the Appwrite Auth / Users / Teams services.
"product / auth":
- "(auth|session|login|logout|register|2fa|mfa|users|teams|memberships|invite|oauth|oauth2|sso|jwt)"
# Fixes and upgrades for the Appwrite Realtime API.
"api / realtime":
- "(realtime|subscribe|websockets)"
# Console, UI and UX issues
"product / console":
- "(console)"
# Fixes and upgrades for the Appwrite Storage.
"product / storage":
- "(storage|bucket|file|image|preview|download)"
# Fixes and upgrades for the Appwrite Database.
"product / databases":
- "(database|collection|tables|attribute|column|document|row|query|queries|indexes|search|filter|sort|pagination)"
# Fixes and upgrades for the Appwrite Functions.
"product / functions":
- "(function|runtime|deployment|execution|trigger|cron|schedule)"
# Fixes and upgrades for the Appwrite Docs.
# "product / docs":
# -
# Fixes and upgrades for the Appwrite Migrations.
"product / migrations":
- "(migrate|migration)"
# Fixes and upgrades for the Appwrite Messaging.
"product / messaging":
- "(messaging|email|sms|push|provider|topic|target|notification)"
# Fixes and upgrades for the Appwrite Platform.
# "product / platform":
# -
# Fixes and upgrades for database relationships
"feature / relationships":
- "(relationship)"
# Issues found only on Appwrite Cloud
# "product / cloud":
# -
# Fixes and upgrades for the Appwrite VCS.
"product / vcs":
- "(repo|push|vcs|repository)"
# Fixes and upgrades for the Appwrite GraphQL API.
"api / graphql":
- "(graphql|gql|mutation)"
# Fixes and upgrades for the Appwrite Assistant.
"product / assistant":
- "(assistant)"
# Fixes and upgrades for the Appwrite Domains.
"product / domains":
- "(domain|dns|ssl|certificate)"
# Fixes and upgrades for the Appwrite Locale.
"product / locale":
- "(locale|i18n|internationalization|localization|l10n|translation|timezone|country)"
# Fixes and upgrades for the Appwrite Avatars.
"product / avatars":
- "(avatar|initial|flag|icon)"
# Fixes and upgrades for Appwrite Sites.
"product / sites":
- "(site|web|hosting|domain|ssl|certificate|nextjs|nuxt|react|angular|vue|svelte|astro)"
# Fixes and upgrades for the Appwrite CLI.
"sdk / cli":
- "(cli|command line)"
# Issues only found when self-hosting Appwrite
"product / self-hosted":
- "(self-host|self host)"

32
.github/workflows/ai-moderator.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: AI Moderator
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
pull_request:
types: [opened, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
discussion:
types: [created, edited]
discussion_comment:
types: [created, edited]
permissions:
models: read
issues: write
pull-requests: write
discussions: write
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: AI Moderator
uses: github/ai-moderator@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

22
.github/workflows/auto-label-issue.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Auto Label Issue
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- name: Issue Labeler
uses: github/issue-labeler@v3.4
with:
configuration-path: .github/labeler.yml
enable-versioned-regex: false
include-title: 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4992
.github/workflows/issue-triage.lock.yml generated vendored Normal file

File diff suppressed because it is too large Load diff

85
.github/workflows/issue-triage.md vendored Normal file
View file

@ -0,0 +1,85 @@
---
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Enable manual trigger
stop-after: +30d # workflow will no longer trigger after 30 days. Remove this and recompile to run indefinitely
reaction: eyes
permissions: read-all
network: defaults
safe-outputs:
add-labels:
max: 5
add-comment:
tools:
web-fetch:
web-search:
timeout_minutes: 10
source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a
---
# Agentic Triage
<!-- Note - this file can be customized to your needs. Replace this section directly, or add further instructions here. After editing run 'gh aw compile' -->
You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them.
1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format).
2. For each issue found, perform the following triage tasks:
3. Select appropriate labels for the issue from the provided list.
4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue.
5. Next, use the GitHub tools to gather additional context about the issue:
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates.
6. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
8. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
- Only select labels from the provided list above
- It's okay to not add any labels if none are clearly applicable
9. Apply the selected labels:
- Use the `update_issue` tool to apply the labels to the issue
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
10. Add an issue comment to the issue with your analysis:
- Start with "🎯 Agentic Issue Triage"
- Provide a brief summary of the issue
- **If duplicate or related issues were found**, add a section listing them with links (e.g., "### 🔗 Potentially Related Issues" followed by a bullet list of related issues with their titles and links)
- Mention any relevant details that might help the team understand the issue better
- Include any debugging strategies or reproduction steps if applicable
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
- Mention any nudges or ideas that could help the team in addressing the issue
- If you have possible reproduction steps, include them in the comment
- If you have any debugging strategies, include them in the comment
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage.

View file

@ -1,266 +1,654 @@
# Version 1.8.0
## What's Changed
### Notable changes
* Do not allow full range in [#9847](https://github.com/appwrite/appwrite/pull/9847)
* Expose internal id as a part of auto increment id in [#9713](https://github.com/appwrite/appwrite/pull/9713)
* Expose sequence in [#9870](https://github.com/appwrite/appwrite/pull/9870)
* Add flutter 3.32 and dart 3.8 runtimes in [#9914](https://github.com/appwrite/appwrite/pull/9914)
* Shorten commit url and branch url in [#9919](https://github.com/appwrite/appwrite/pull/9919)
* Remove powered by from error pages in [#9927](https://github.com/appwrite/appwrite/pull/9927)
* Enable resource limits on GIF previews in [#9940](https://github.com/appwrite/appwrite/pull/9940)
* Only run maintenance task for projects accessed in last 24 hours in [#9989](https://github.com/appwrite/appwrite/pull/9989)
* Add increment + decrement routes in [#9986](https://github.com/appwrite/appwrite/pull/9986)
* Only run maintenance task for projects accessed in last 30 days in [#9995](https://github.com/appwrite/appwrite/pull/9995)
* Update appwrite-assistant image version to 0.8.3 in [#10003](https://github.com/appwrite/appwrite/pull/10003)
* Update emails to use button in [#9590](https://github.com/appwrite/appwrite/pull/9590)
* Create commit & branch url for first git deployment when site is linked to repo in [#9969](https://github.com/appwrite/appwrite/pull/9969)
* Handle React Native schemes in [#9650](https://github.com/appwrite/appwrite/pull/9650)
* Handle origin validation for web extensions in [#10107](https://github.com/appwrite/appwrite/pull/10107)
* Preview text for emails in [#10198](https://github.com/appwrite/appwrite/pull/10198)
* Create email target when using email OTP registration in [#10224](https://github.com/appwrite/appwrite/pull/10224)
* Add CSV imports in [#10231](https://github.com/appwrite/appwrite/pull/10231)
* Add support for svg favicons in [#10255](https://github.com/appwrite/appwrite/pull/10255)
* Realtime support for bulk api in [#10096](https://github.com/appwrite/appwrite/pull/10096)
* Skip redundant subqueries in users list route in [#10297](https://github.com/appwrite/appwrite/pull/10297)
* Add native sign in with Apple function template in [#10286](https://github.com/appwrite/appwrite/pull/10286)
* Add support for HEAD requests in [#10304](https://github.com/appwrite/appwrite/pull/10304)
* Update invite email copy in [#10309](https://github.com/appwrite/appwrite/pull/10309)
* Increase dynamic API key expiration in [#10328](https://github.com/appwrite/appwrite/pull/10328)
* Add TablesDB service in [#10333](https://github.com/appwrite/appwrite/pull/10333)
* Add execution.deploymentId to response model in [#10357](https://github.com/appwrite/appwrite/pull/10357)
* Switch Union China Pay to just Union Pay in [#10372](https://github.com/appwrite/appwrite/pull/10372) and [#10382](https://github.com/appwrite/appwrite/pull/10382)
* Add execution id and log id to response headers in [#10379](https://github.com/appwrite/appwrite/pull/10379)
* Add executionId and client IP to function headers in [#9147](https://github.com/appwrite/appwrite/pull/9147)
* Allow HEAD requests in function executions in [#10385](https://github.com/appwrite/appwrite/pull/10385)
* Add support for select queries when listing deployments in [#10380](https://github.com/appwrite/appwrite/pull/10380)
* Add spatial type attributes in [#10356](https://github.com/appwrite/appwrite/pull/10356) and [#10443](https://github.com/appwrite/appwrite/pull/10443)
* Add realtime support for bulk upserts in [#10425](https://github.com/appwrite/appwrite/pull/10425)
* Add previewUrl to vcs comment from vcs controller in [#10396](https://github.com/appwrite/appwrite/pull/10396)
* Rename verification SDK methods to be more specific in [#10606](https://github.com/appwrite/appwrite/pull/10606)
* Add project name in email subject in [#10609](https://github.com/appwrite/appwrite/pull/10609)
* Throw error when email is not available for account verification in [#10533](https://github.com/appwrite/appwrite/pull/10533)
* Add support for transactions in [#10023](https://github.com/appwrite/appwrite/pull/10023) and [#10624](https://github.com/appwrite/appwrite/pull/10624)
* Use bcc only emails for smtp in [#10644](https://github.com/appwrite/appwrite/pull/10644)
### Fixes
* Fix rules on active deployment in [#9902](https://github.com/appwrite/appwrite/pull/9902)
* Fix for upserts with differing optional parameter sets in [#9928](https://github.com/appwrite/appwrite/pull/9928)
* Fix teams deletion in [#9888](https://github.com/appwrite/appwrite/pull/9888)
* Fix deletion logic in [#9938](https://github.com/appwrite/appwrite/pull/9938)
* Update database for upsert fix in [#9941](https://github.com/appwrite/appwrite/pull/9941)
* Fix expire format in account recovery, verification, phone and mfa in [#9600](https://github.com/appwrite/appwrite/pull/9600)
* Fix github comments and deployment creation on branch deletion in [#9949](https://github.com/appwrite/appwrite/pull/9949)
* Fix cache issues with proxy for deployment download in [#9971](https://github.com/appwrite/appwrite/pull/9971)
* Redirect rule parent resource in [#9982](https://github.com/appwrite/appwrite/pull/9982)
* Fix usage queues in [#9946](https://github.com/appwrite/appwrite/pull/9946)
* Transfer control for the migration in [#9997](https://github.com/appwrite/appwrite/pull/9997)
* Prevent 'Attribute "factors" must be an array' error in [#10004](https://github.com/appwrite/appwrite/pull/10004)
* Fix all vcs urls missing region in [#9998](https://github.com/appwrite/appwrite/pull/9998)
* Add readable error for csv imports in [#9947](https://github.com/appwrite/appwrite/pull/9947)
* Fix missing screenshot logs in [#10024](https://github.com/appwrite/appwrite/pull/10024)
* Update executor to fix s3 endpoint bug in [#10036](https://github.com/appwrite/appwrite/pull/10036)
* Fix build duration calculation in [#10053](https://github.com/appwrite/appwrite/pull/10053)
* Fix logs order in [#10052](https://github.com/appwrite/appwrite/pull/10052)
* Fix platform check for Sites with automatic rule in [#10043](https://github.com/appwrite/appwrite/pull/10043)
* Increase cache ttl to ensure hits in [#10079](https://github.com/appwrite/appwrite/pull/10079)
* Fix connect to existing repo flow in [#10034](https://github.com/appwrite/appwrite/pull/10034)
* Fix migrations path and type in [#10090](https://github.com/appwrite/appwrite/pull/10090)
* Fix JWT authentication database selection for admin mode in [#10098](https://github.com/appwrite/appwrite/pull/10098)
* Use _APP_CONSOLE_DOMAIN, if not found, then use _APP_DOMAIN in [#9999](https://github.com/appwrite/appwrite/pull/9999)
* Fix file tokens not working on file-security in [#10120](https://github.com/appwrite/appwrite/pull/10120)
* Fix build activation race condition in [#9952](https://github.com/appwrite/appwrite/pull/9952)
* Changed the default permission param of upsert document in [#10129](https://github.com/appwrite/appwrite/pull/10129)
* Fix success validation in oauth2 redirect in [#10130](https://github.com/appwrite/appwrite/pull/10130)
* Update OAuth2 redirect URLs in [#10119](https://github.com/appwrite/appwrite/pull/10119)
* Fix specs with new env vars in [#10135](https://github.com/appwrite/appwrite/pull/10135)
* Skip deployment when commit is created by us in [#10187](https://github.com/appwrite/appwrite/pull/10187)
* Use direct source for file-preview when empty in [#10181](https://github.com/appwrite/appwrite/pull/10181)
* Better error message for invalid function scheduled time in [#10201](https://github.com/appwrite/appwrite/pull/10201)
* Add defaultBranch in getRepository response in [#10190](https://github.com/appwrite/appwrite/pull/10190)
* Filter sequence to int because any models skip rule checks in [#10221](https://github.com/appwrite/appwrite/pull/10221)
* Fix 500 errors on robots and humans txt files in [#10248](https://github.com/appwrite/appwrite/pull/10248)
* Fix atomic number ops with limit 0 in [#10264](https://github.com/appwrite/appwrite/pull/10264)
* Update build command for flutter in [#10288](https://github.com/appwrite/appwrite/pull/10288)
* Add a fallback locale in [#10307](https://github.com/appwrite/appwrite/pull/10307)
* Fix variables sharing across resources in [#10308](https://github.com/appwrite/appwrite/pull/10308)
* Fix uncaught invalid arg in [#10318](https://github.com/appwrite/appwrite/pull/10318)
* Add missing upsert event in [#10317](https://github.com/appwrite/appwrite/pull/10317)
* Improve font reliability in [#10332](https://github.com/appwrite/appwrite/pull/10332)
* Truncate logs in function worker in [#9773](https://github.com/appwrite/appwrite/pull/9773)
* Fix event template configuration issues in [#10350](https://github.com/appwrite/appwrite/pull/10350)
* Fix users events & missed publisher logic for Functions in [#10348](https://github.com/appwrite/appwrite/pull/10348)
* Fix incorrect file token expiry in [#10329](https://github.com/appwrite/appwrite/pull/10329)
* Fix upserting that makes no change in [#10363](https://github.com/appwrite/appwrite/pull/10363) and [#10364](https://github.com/appwrite/appwrite/pull/10364)
* Fix domain validator in [#10374](https://github.com/appwrite/appwrite/pull/10374)
* Apply sequence integer casting and attribute cleanup fixes to Row model, TablesDB tests, and document processing in [#10383](https://github.com/appwrite/appwrite/pull/10383)
* Fix domain validator in [#10386](https://github.com/appwrite/appwrite/pull/10386)
* Fix sequence removal in [#10388](https://github.com/appwrite/appwrite/pull/10388)
* Fix TablesDB scopes in [#10387](https://github.com/appwrite/appwrite/pull/10387)
* Fix request filter in [#10389](https://github.com/appwrite/appwrite/pull/10389)
* Fix nested filter selects in [#10393](https://github.com/appwrite/appwrite/pull/10393)
* Fix readonly attr stripping on write in [#10405](https://github.com/appwrite/appwrite/pull/10405)
* Replace %s with mustache placeholder in [#10392](https://github.com/appwrite/appwrite/pull/10392)
* Support array headers for set-cookie in [#10427](https://github.com/appwrite/appwrite/pull/10427)
* Fix put prefs structure validation in [#10436](https://github.com/appwrite/appwrite/pull/10436)
* Fix oauth identity check in [#10460](https://github.com/appwrite/appwrite/pull/10460)
* Fix check in [#10489](https://github.com/appwrite/appwrite/pull/10489)
* Fix database usage metrics in [#10483](https://github.com/appwrite/appwrite/pull/10483)
* Throw appropriate 400s from request filters in [#10502](https://github.com/appwrite/appwrite/pull/10502)
* Catch query exception on bucket/file list in [#10505](https://github.com/appwrite/appwrite/pull/10505)
* Use outputDirectory attribute from deployment in [#10571](https://github.com/appwrite/appwrite/pull/10571)
* Fix buildOutput attribute name in deployment check in [#10572](https://github.com/appwrite/appwrite/pull/10572)
* Update database for nested selection fix in [#10577](https://github.com/appwrite/appwrite/pull/10577)
* Auto-allow sites domain for OAuth in [#10503](https://github.com/appwrite/appwrite/pull/10503)
* Handle OIDC well-known endpoint errors in [#10589](https://github.com/appwrite/appwrite/pull/10589)
* Correct invalid template links in Create temporary deployment endpoint in [#10581](https://github.com/appwrite/appwrite/pull/10581)
* Update broken create table links in TablesDB docs in [#10592](https://github.com/appwrite/appwrite/pull/10592)
* Fix cross API compatibility in [#10626](https://github.com/appwrite/appwrite/pull/10626)
* Fix code 0 from databases on realtime in [#10631](https://github.com/appwrite/appwrite/pull/10631)
* Throw duplicate error when function id already exists in [#10618](https://github.com/appwrite/appwrite/pull/10618)
### Miscellaneous
* Fix task coroutine hooks in [#9850](https://github.com/appwrite/appwrite/pull/9850)
* Feat sync encrypt updates in [#9871](https://github.com/appwrite/appwrite/pull/9871)
* Revert "Feat sync encrypt updates" in [#9877](https://github.com/appwrite/appwrite/pull/9877)
* Add builds worker group in [#9872](https://github.com/appwrite/appwrite/pull/9872)
* Revert encrypted attribute changes in [#9898](https://github.com/appwrite/appwrite/pull/9898)
* Update sdk generator and sdks in [#9849](https://github.com/appwrite/appwrite/pull/9849)
* Release cli in [#9900](https://github.com/appwrite/appwrite/pull/9900)
* Improve how rules are fetched in [#9915](https://github.com/appwrite/appwrite/pull/9915)
* Sync 1.6 in [#9920](https://github.com/appwrite/appwrite/pull/9920)
* Update messaging library in [#9764](https://github.com/appwrite/appwrite/pull/9764)
* Disable TCP hook on stats resources in [#9932](https://github.com/appwrite/appwrite/pull/9932)
* Remove JSON index on roles due to MySQL bug in [#9924](https://github.com/appwrite/appwrite/pull/9924)
* Update queue in [#9936](https://github.com/appwrite/appwrite/pull/9936)
* Fix flaky account tests in [#9954](https://github.com/appwrite/appwrite/pull/9954)
* Fix flaky messaging test in [#9957](https://github.com/appwrite/appwrite/pull/9957)
* Make usage tests robust in [#9956](https://github.com/appwrite/appwrite/pull/9956)
* Increase deployment timeouts in tests in [#9955](https://github.com/appwrite/appwrite/pull/9955)
* Graceful shutdown on SIGTERM in [#9890](https://github.com/appwrite/appwrite/pull/9890)
* Bring back telemetry for storage in [#9903](https://github.com/appwrite/appwrite/pull/9903)
* Update version to 1.7.4 and add experimental warnings in [#9959](https://github.com/appwrite/appwrite/pull/9959)
* Return queue pre-fetch results in [#9731](https://github.com/appwrite/appwrite/pull/9731)
* Update SDK versions in [#9987](https://github.com/appwrite/appwrite/pull/9987)
* Restore unique filename for health check #9842 in [#9993](https://github.com/appwrite/appwrite/pull/9993)
* Add after build hook in [#9996](https://github.com/appwrite/appwrite/pull/9996)
* Remove endpoint selector in [#10000](https://github.com/appwrite/appwrite/pull/10000)
* Use static code instead of astro in tests in [#9966](https://github.com/appwrite/appwrite/pull/9966)
* Add ref param to vcs list contents in [#9991](https://github.com/appwrite/appwrite/pull/9991)
* Update coderabbit config file in [#10005](https://github.com/appwrite/appwrite/pull/10005)
* TAR support in [#10016](https://github.com/appwrite/appwrite/pull/10016)
* Update delete project scope in [#10017](https://github.com/appwrite/appwrite/pull/10017)
* Lazy-load relationships in [#9669](https://github.com/appwrite/appwrite/pull/9669)
* Revert "Feat: Lazy-load relationships" in [#10018](https://github.com/appwrite/appwrite/pull/10018)
* Revert "Update delete project scope" in [#10022](https://github.com/appwrite/appwrite/pull/10022)
* 1.8.x in [#9985](https://github.com/appwrite/appwrite/pull/9985)
* Update cli version and add bulk operation warnings in [#10007](https://github.com/appwrite/appwrite/pull/10007)
* Update Appwrite description to include Sites, add MCP to products list in [#9867](https://github.com/appwrite/appwrite/pull/9867)
* Update README.md in [#10026](https://github.com/appwrite/appwrite/pull/10026)
* Fix duplication of platforms in swagger specs in [#10008](https://github.com/appwrite/appwrite/pull/10008)
* Update react native sdk and changelog in [#10025](https://github.com/appwrite/appwrite/pull/10025)
* Update delete project signature in [#10028](https://github.com/appwrite/appwrite/pull/10028)
* Fix Golang SDK examples for docs in [#10001](https://github.com/appwrite/appwrite/pull/10001)
* Revert "worker: Graceful shutdown on SIGTERM" in [#10035](https://github.com/appwrite/appwrite/pull/10035)
* Fix benchmark CI in [#10055](https://github.com/appwrite/appwrite/pull/10055)
* Use ->action(...)) instead of ->callback([$this, 'action']); in [#9967](https://github.com/appwrite/appwrite/pull/9967)
* Override project via custom domains log in [#10011](https://github.com/appwrite/appwrite/pull/10011)
* Add database worker job logging in [#10056](https://github.com/appwrite/appwrite/pull/10056)
* Add runtimeEntrypoint param in [#10062](https://github.com/appwrite/appwrite/pull/10062)
* Add missing injections in [#10061](https://github.com/appwrite/appwrite/pull/10061)
* Replace Console loop with Swoole Timer for stats resource m… in [#10054](https://github.com/appwrite/appwrite/pull/10054)
* Update README.md in [#10063](https://github.com/appwrite/appwrite/pull/10063)
* Fix parameter order in action function for robots.txt route in [#10067](https://github.com/appwrite/appwrite/pull/10067)
* Preview endpoint logging in [#10068](https://github.com/appwrite/appwrite/pull/10068)
* Fix flakyness of account tests in [#10066](https://github.com/appwrite/appwrite/pull/10066)
* Update cli to 8.1.0 and add changelog in [#10070](https://github.com/appwrite/appwrite/pull/10070)
* Update composer.json and composer.lock to include appwrite-lab… in [#10051](https://github.com/appwrite/appwrite/pull/10051)
* Fix tests, for `Cloud` in [#10085](https://github.com/appwrite/appwrite/pull/10085)
* Update README.md in [#10084](https://github.com/appwrite/appwrite/pull/10084)
* Revert "chore: update composer.json and composer.lock to include appwrite-lab…" in [#10086](https://github.com/appwrite/appwrite/pull/10086)
* Update README to add Bulk API link in [#10095](https://github.com/appwrite/appwrite/pull/10095)
* Add redis publisher to schedule base if available in [#10099](https://github.com/appwrite/appwrite/pull/10099)
* Fix site template test in [#10104](https://github.com/appwrite/appwrite/pull/10104)
* Update nodejs 17.1.0 in [#10088](https://github.com/appwrite/appwrite/pull/10088)
* Update README.md to add Upsert announcement in [#10112](https://github.com/appwrite/appwrite/pull/10112)
* Reduce delete batch size in [#10128](https://github.com/appwrite/appwrite/pull/10128)
* Update README.md in [#10134](https://github.com/appwrite/appwrite/pull/10134)
* Speed up tests in [#10127](https://github.com/appwrite/appwrite/pull/10127)
* Update cli to 8.2.0 in [#10136](https://github.com/appwrite/appwrite/pull/10136)
* Prevent injected $user from being shadowed in [#10150](https://github.com/appwrite/appwrite/pull/10150)
* Update react native to 0.10.1 and dotnet to 0.14.0 in [#10138](https://github.com/appwrite/appwrite/pull/10138)
* Update README.md in [#10153](https://github.com/appwrite/appwrite/pull/10153)
* Update cli 8.2.1 in [#10155](https://github.com/appwrite/appwrite/pull/10155)
* Fix build usage specification in [#10157](https://github.com/appwrite/appwrite/pull/10157)
* Handle redirect validator in specs + GraphQL type mapper in [#10158](https://github.com/appwrite/appwrite/pull/10158)
* Update dart 16.1.0, flutter 17.0.2 and cli 8.2.2 in [#10161](https://github.com/appwrite/appwrite/pull/10161)
* Improve invalid scheme error in origin check in [#10164](https://github.com/appwrite/appwrite/pull/10164)
* 1.7.x in [#9897](https://github.com/appwrite/appwrite/pull/9897)
* Added the cases of null permissions in the upsert route and update th… in [#10179](https://github.com/appwrite/appwrite/pull/10179)
* Fix 1.7.x specs in [#10197](https://github.com/appwrite/appwrite/pull/10197)
* Suppress git-action exception in deployment worker in [#10199](https://github.com/appwrite/appwrite/pull/10199)
* Stats-usage on redis in [#10156](https://github.com/appwrite/appwrite/pull/10156)
* Fix templates on `1.7.x`. in [#10203](https://github.com/appwrite/appwrite/pull/10203)
* Change preview & body for MFA email in [#10205](https://github.com/appwrite/appwrite/pull/10205)
* Add docs for nestedType, encode, from and toMap in [#10204](https://github.com/appwrite/appwrite/pull/10204)
* Update sdks 1.7.x in [#10202](https://github.com/appwrite/appwrite/pull/10202)
* Update migration release in [#10222](https://github.com/appwrite/appwrite/pull/10222)
* Remove sequence on incoming docs in [#10228](https://github.com/appwrite/appwrite/pull/10228)
* Filter certificates renewal task in maintenance by region in [#10227](https://github.com/appwrite/appwrite/pull/10227)
* Move changelog to sdks platforms array in [#10233](https://github.com/appwrite/appwrite/pull/10233)
* Update changelog and sdk gen in [#10247](https://github.com/appwrite/appwrite/pull/10247)
* Telemetry for cache hits and misses in [#10240](https://github.com/appwrite/appwrite/pull/10240)
* Add model examples + additonal examples to specs in [#10249](https://github.com/appwrite/appwrite/pull/10249)
* Update favicons endpoint to fallback to ico instead of throwing error in [#10260](https://github.com/appwrite/appwrite/pull/10260)
* Update README.md in [#10259](https://github.com/appwrite/appwrite/pull/10259)
* Check CAA record before issuing certificate in [#10258](https://github.com/appwrite/appwrite/pull/10258)
* Revert "Check CAA record before issuing certificate" in [#10263](https://github.com/appwrite/appwrite/pull/10263)
* Test var id attribute in [#10243](https://github.com/appwrite/appwrite/pull/10243)
* Add type attribute to the database creation flow in [#10266](https://github.com/appwrite/appwrite/pull/10266)
* Add CAA validator in [#10267](https://github.com/appwrite/appwrite/pull/10267)
* Update database type to grids and legacy in [#10273](https://github.com/appwrite/appwrite/pull/10273)
* Update README.md in [#10272](https://github.com/appwrite/appwrite/pull/10272)
* Upgrade composer for utopia migration in [#10274](https://github.com/appwrite/appwrite/pull/10274)
* Update SDK generator and sdks in [#10271](https://github.com/appwrite/appwrite/pull/10271)
* Fix wrong resource path for audits in [#10279](https://github.com/appwrite/appwrite/pull/10279)
* Update `grid` on resource events in [#10282](https://github.com/appwrite/appwrite/pull/10282)
* Add readonly param to sequence, databaseId and collectionId in [#10278](https://github.com/appwrite/appwrite/pull/10278)
* Update migrations in [#10283](https://github.com/appwrite/appwrite/pull/10283)
* Add placeholder detection in [#10284](https://github.com/appwrite/appwrite/pull/10284)
* Update docker base to 0.10.3 in [#10285](https://github.com/appwrite/appwrite/pull/10285)
* Make check for adding warning header stricter in [#10293](https://github.com/appwrite/appwrite/pull/10293)
* Fix databases worker cache clearing bug in [#10294](https://github.com/appwrite/appwrite/pull/10294)
* Reapply Redis functions queue in [#10299](https://github.com/appwrite/appwrite/pull/10299)
* Add new database query type tests in [#10296](https://github.com/appwrite/appwrite/pull/10296)
* Update package in [#10312](https://github.com/appwrite/appwrite/pull/10312)
* Update required attributes in [#10311](https://github.com/appwrite/appwrite/pull/10311)
* Remove experiment warnings from bulk methods in [#10310](https://github.com/appwrite/appwrite/pull/10310)
* Update README.md in [#10313](https://github.com/appwrite/appwrite/pull/10313)
* Added internal file param to handle upload to internal bucket in [#10321](https://github.com/appwrite/appwrite/pull/10321)
* Remove temp logging in [#10302](https://github.com/appwrite/appwrite/pull/10302)
* Improve sites test for stability in [#10331](https://github.com/appwrite/appwrite/pull/10331)
* Database lib bump to 0.71.15 in [#10336](https://github.com/appwrite/appwrite/pull/10336)
* Clarify userId param in endpoints that create accounts in [#10117](https://github.com/appwrite/appwrite/pull/10117)
* Upgrade HTTP in [#10338](https://github.com/appwrite/appwrite/pull/10338)
* Remove unnessessary external dependnecies in [#10343](https://github.com/appwrite/appwrite/pull/10343)
* Sync main into 1.7.x in [#10347](https://github.com/appwrite/appwrite/pull/10347)
* Fix TablesDB casing in [#10346](https://github.com/appwrite/appwrite/pull/10346)
* Add cookies test in [#10352](https://github.com/appwrite/appwrite/pull/10352)
* Update token tests with jwt decode in [#10354](https://github.com/appwrite/appwrite/pull/10354)
* Utilize assets server for fonts in [#10358](https://github.com/appwrite/appwrite/pull/10358)
* Sync main into 1.7.x in [#10359](https://github.com/appwrite/appwrite/pull/10359)
* Bump 1.7.x in [#10365](https://github.com/appwrite/appwrite/pull/10365)
* Fix queue health in [#10369](https://github.com/appwrite/appwrite/pull/10369)
* Allow publisher messaging override in scheduler in [#10370](https://github.com/appwrite/appwrite/pull/10370)
* Add replacewith and deprecated since to account methods in [#10377](https://github.com/appwrite/appwrite/pull/10377)
* Update README.md in [#10376](https://github.com/appwrite/appwrite/pull/10376)
* Update CLI in [#10390](https://github.com/appwrite/appwrite/pull/10390)
* Update default method in description in [#10391](https://github.com/appwrite/appwrite/pull/10391)
* Rename namespace from tables-db to tablesdb in specs in [#10395](https://github.com/appwrite/appwrite/pull/10395)
* Update tables group in specs in [#10394](https://github.com/appwrite/appwrite/pull/10394)
* Update description for upsert methods in [#10397](https://github.com/appwrite/appwrite/pull/10397)
* Update README.md in [#10401](https://github.com/appwrite/appwrite/pull/10401)
* Added handling of database resources after migration in [#10400](https://github.com/appwrite/appwrite/pull/10400)
* Revert "Added handling of database resources after migration" in [#10406](https://github.com/appwrite/appwrite/pull/10406)
* Remove sdk deprecation warnings in [#10408](https://github.com/appwrite/appwrite/pull/10408)
* Mark Row response model's param with readonly in [#10409](https://github.com/appwrite/appwrite/pull/10409)
* Update exception thrown when svg sanitization fails in [#10416](https://github.com/appwrite/appwrite/pull/10416)
* Fix allow null params in [#10417](https://github.com/appwrite/appwrite/pull/10417)
* Allow running tests with specific response format in [#10418](https://github.com/appwrite/appwrite/pull/10418)
* Make webhooks publisher overridable in [#10419](https://github.com/appwrite/appwrite/pull/10419)
* Check audits logs in [#10414](https://github.com/appwrite/appwrite/pull/10414)
* Remove direct publisher calls in [#10420](https://github.com/appwrite/appwrite/pull/10420)
* removed spatial type response and will be using the json type for the… in [#10433](https://github.com/appwrite/appwrite/pull/10433)
* Add tests for new time helpers in [#10437](https://github.com/appwrite/appwrite/pull/10437)
* Move projects.list() to module in [#10441](https://github.com/appwrite/appwrite/pull/10441)
* Update cli to 9.1.0 in [#10442](https://github.com/appwrite/appwrite/pull/10442)
* Add requestBody param examples in specs in [#10431](https://github.com/appwrite/appwrite/pull/10431)
* Fix mysql tests in [#10445](https://github.com/appwrite/appwrite/pull/10445)
* Upgrade platform lib to have older queue lib in [#10447](https://github.com/appwrite/appwrite/pull/10447)
* Fix router compression in [#10452](https://github.com/appwrite/appwrite/pull/10452)
* Upgrade http lib for backwards compatible default param in [#10455](https://github.com/appwrite/appwrite/pull/10455)
* Update examples in [#10444](https://github.com/appwrite/appwrite/pull/10444)
* Automatic pr creation in sdk release script in [#10457](https://github.com/appwrite/appwrite/pull/10457)
* Remove avatars command from cli in [#10454](https://github.com/appwrite/appwrite/pull/10454)
* Remove deno from platforms array in [#10453](https://github.com/appwrite/appwrite/pull/10453)
* Spatial type attributes sdk updates in [#10463](https://github.com/appwrite/appwrite/pull/10463)
* Stats resources try catch in [#10469](https://github.com/appwrite/appwrite/pull/10469)
* Move proxy endpoints to modules in [#10470](https://github.com/appwrite/appwrite/pull/10470)
* Add certificate validation override in [#10471](https://github.com/appwrite/appwrite/pull/10471)
* Generate SDKs in [#10475](https://github.com/appwrite/appwrite/pull/10475)
* Spatial test tablesdb updates in [#10473](https://github.com/appwrite/appwrite/pull/10473)
* Add colors to certificate logs in [#10438](https://github.com/appwrite/appwrite/pull/10438)
* appwrite db bump in [#10479](https://github.com/appwrite/appwrite/pull/10479)
* Bump database in [#10480](https://github.com/appwrite/appwrite/pull/10480)
* Health db queues in [#10482](https://github.com/appwrite/appwrite/pull/10482)
* Attempt small size for website dependency in [#10485](https://github.com/appwrite/appwrite/pull/10485)
* Worker stop in [#10498](https://github.com/appwrite/appwrite/pull/10498)
* Update database in [#10506](https://github.com/appwrite/appwrite/pull/10506)
* Stats resources and usage sorting by unique field in [#10472](https://github.com/appwrite/appwrite/pull/10472)
* Add spatial column validation during required mode and tests for exis… in [#10509](https://github.com/appwrite/appwrite/pull/10509)
* Sub query variables order by in [#10513](https://github.com/appwrite/appwrite/pull/10513)
* Update README.md in [#10514](https://github.com/appwrite/appwrite/pull/10514)
* bump database 1.5.0 in [#10515](https://github.com/appwrite/appwrite/pull/10515)
* Don't remove required attributes in [#10516](https://github.com/appwrite/appwrite/pull/10516)
* Catch query exception on bulk update/delete in [#10517](https://github.com/appwrite/appwrite/pull/10517)
* Update cli to 10.0.0 in [#10511](https://github.com/appwrite/appwrite/pull/10511)
* Add type_enum support and update docs in [#10496](https://github.com/appwrite/appwrite/pull/10496)
* Improve code readability for schedules in [#10522](https://github.com/appwrite/appwrite/pull/10522)
* Include response model enum names in [#10538](https://github.com/appwrite/appwrite/pull/10538)
* SDK releases in [#10539](https://github.com/appwrite/appwrite/pull/10539)
* Fix health status enum in [#10540](https://github.com/appwrite/appwrite/pull/10540)
* Update afterbuild fn in [#10541](https://github.com/appwrite/appwrite/pull/10541)
* Update afterbuild to also pass adapter in [#10545](https://github.com/appwrite/appwrite/pull/10545)
* Update `z-index` to be the highest in [#9874](https://github.com/appwrite/appwrite/pull/9874)
* Update framework lib to 0.33.28 in [#10551](https://github.com/appwrite/appwrite/pull/10551)
* Fix enum typing for platform in specs in [#10553](https://github.com/appwrite/appwrite/pull/10553)
* Add enums for database type and column status in [#10561](https://github.com/appwrite/appwrite/pull/10561)
* Fix activities in [#10586](https://github.com/appwrite/appwrite/pull/10586)
* Fix logs truncation tests in [#10585](https://github.com/appwrite/appwrite/pull/10585)
* Remove related data in realtime payload in [#10590](https://github.com/appwrite/appwrite/pull/10590)
* Update composer dependencies in [#10601](https://github.com/appwrite/appwrite/pull/10601)
* Update sdks add response models in [#10554](https://github.com/appwrite/appwrite/pull/10554)
* Sanitize 5xx errors on realtime in [#10598](https://github.com/appwrite/appwrite/pull/10598)
* Update database in [#10596](https://github.com/appwrite/appwrite/pull/10596)
* Add both collection and table id in the realtime in [#10608](https://github.com/appwrite/appwrite/pull/10608)
* Chore bump db in [#10611](https://github.com/appwrite/appwrite/pull/10611)
* Branded email for Console auth flows in [#10501](https://github.com/appwrite/appwrite/pull/10501)
* Add minor releases for all SDKs - deprecate createVerification, add createEmailVerification in [#10614](https://github.com/appwrite/appwrite/pull/10614)
* Add automatic releases in [#10615](https://github.com/appwrite/appwrite/pull/10615)
* Feat txn sdks in [#10621](https://github.com/appwrite/appwrite/pull/10621)
* Prevent empty releases in sdk release script in [#10627](https://github.com/appwrite/appwrite/pull/10627)
* Update domains lib to 0.8.2 in [#10629](https://github.com/appwrite/appwrite/pull/10629)
* Fix txn API scope backwards compat in [#10640](https://github.com/appwrite/appwrite/pull/10640)
* Fix block schedules in [#10620](https://github.com/appwrite/appwrite/pull/10620)
* Update .NET SDK to 0.21.2 and improve release detection in [#10641](https://github.com/appwrite/appwrite/pull/10641)
* Make methods protected for extending in [#10617](https://github.com/appwrite/appwrite/pull/10617)
# Version 1.7.4
## What's Changed
### Notable changes
* Update console image to version 6.0.13 in [#9891](https://github.com/appwrite/appwrite/pull/9891)
### Fixes
* Fix createDeployment chunk upload in [#9886](https://github.com/appwrite/appwrite/pull/9886)
### Miscellaneous
* Update version from 1.7.3 to 1.7.4 in [#9893](https://github.com/appwrite/appwrite/pull/9893)
# Version 1.7.3
## What's Changed
### Notable changes
* Allow unlimited deployment size in [#9866](https://github.com/appwrite/appwrite/pull/9866)
* Bump console to version 6.0.11 in [#9881](https://github.com/appwrite/appwrite/pull/9881)
### Fixes
* Send deploymentResourceType in rules verification in [#9859](https://github.com/appwrite/appwrite/pull/9859)
* Fix CNAME validation in [#9861](https://github.com/appwrite/appwrite/pull/9861)
* Fix bucket not included in path in [#9864](https://github.com/appwrite/appwrite/pull/9864)
* Fix URL for view logs in github comment in [#9875](https://github.com/appwrite/appwrite/pull/9875)
* Set owner and region while migrating rules in [#9856](https://github.com/appwrite/appwrite/pull/9856)
* Remove _APP_DEFAULT_REGION because it is not a valid env var in [#9883](https://github.com/appwrite/appwrite/pull/9883)
### Miscellaneous
* Only load error page for development mode in [#9860](https://github.com/appwrite/appwrite/pull/9860)
* Make max deployment and build size configurable in [#9863](https://github.com/appwrite/appwrite/pull/9863)
* Update flutter_web_auth_2 docs to match 4.x in [#9858](https://github.com/appwrite/appwrite/pull/9858)
* Use unique filename for health check in [#9842](https://github.com/appwrite/appwrite/pull/9842)
* Added encrypt property in the attribute string response model in [#9868](https://github.com/appwrite/appwrite/pull/9868)
* Add sequence in [#9865](https://github.com/appwrite/appwrite/pull/9865)
* Add builds worker group in [#9873](https://github.com/appwrite/appwrite/pull/9873)
* updated errro for the string encryption in [#9878](https://github.com/appwrite/appwrite/pull/9878)
* Revert "Add sequence" in [#9879](https://github.com/appwrite/appwrite/pull/9879)
* Prepare 1.7.3 release in [#9882](https://github.com/appwrite/appwrite/pull/9882)
# Version 1.6.2
## What's Changed
### Notable changes
* Delete git folder to reduce build size in [9076](https://github.com/appwrite/appwrite/pull/9076)
* Upgrade assistant in [9100](https://github.com/appwrite/appwrite/pull/9100)
* Use redis adapter for abuse in [9121](https://github.com/appwrite/appwrite/pull/9121)
* Set base specification CPUs to 0.5 again in [9146](https://github.com/appwrite/appwrite/pull/9146)
* Add new push message parameters in [9060](https://github.com/appwrite/appwrite/pull/9060)
* Update audits to include user type in [9211](https://github.com/appwrite/appwrite/pull/9211)
* Enable HEIC in [9251](https://github.com/appwrite/appwrite/pull/9251)
* Added teamName to membership redirect url in [9269](https://github.com/appwrite/appwrite/pull/9269)
* Add support endpoint url for S3 in [9303](https://github.com/appwrite/appwrite/pull/9303)
* Added RuPay Credit Card Icon in Avatars Service in [5046](https://github.com/appwrite/appwrite/pull/5046)
* Add figma oauth provider in [9623](https://github.com/appwrite/appwrite/pull/9623)
* Update console to version 5.2.58 in [9637](https://github.com/appwrite/appwrite/pull/9637)
* Delete git folder to reduce build size in [#9076](https://github.com/appwrite/appwrite/pull/9076)
* Upgrade assistant in [#9100](https://github.com/appwrite/appwrite/pull/9100)
* Use redis adapter for abuse in [#9121](https://github.com/appwrite/appwrite/pull/9121)
* Set base specification CPUs to 0.5 again in [#9146](https://github.com/appwrite/appwrite/pull/9146)
* Add new push message parameters in [#9060](https://github.com/appwrite/appwrite/pull/9060)
* Update audits to include user type in [#9211](https://github.com/appwrite/appwrite/pull/9211)
* Enable HEIC in [#9251](https://github.com/appwrite/appwrite/pull/9251)
* Added teamName to membership redirect url in [#9269](https://github.com/appwrite/appwrite/pull/9269)
* Add support endpoint url for S3 in [#9303](https://github.com/appwrite/appwrite/pull/9303)
* Added RuPay Credit Card Icon in Avatars Service in [#5046](https://github.com/appwrite/appwrite/pull/5046)
* Add figma oauth provider in [#9623](https://github.com/appwrite/appwrite/pull/9623)
* Update console to version 5.2.58 in [#9637](https://github.com/appwrite/appwrite/pull/9637)
### Fixes
* Remove failed attribute in [9032](https://github.com/appwrite/appwrite/pull/9032)
* Fix delete notFound attribute in [9038](https://github.com/appwrite/appwrite/pull/9038)
* 🇮🇸 Added missing Icelandic translations for email strings. in [4848](https://github.com/appwrite/appwrite/pull/4848)
* fix doc comment for filter method in [5769](https://github.com/appwrite/appwrite/pull/5769)
* Delete attribute No throwing Exception on not found in [9157](https://github.com/appwrite/appwrite/pull/9157)
* Fix VCS identity collision in [9138](https://github.com/appwrite/appwrite/pull/9138)
* Fix disabling of email-otp when user wants to in [9200](https://github.com/appwrite/appwrite/pull/9200)
* Ensure user can delete session in [9209](https://github.com/appwrite/appwrite/pull/9209)
* Fix resend invitation in [9218](https://github.com/appwrite/appwrite/pull/9218)
* Fix phone number parsing exception handling in [9246](https://github.com/appwrite/appwrite/pull/9246)
* Fix amazon oauth in [9253](https://github.com/appwrite/appwrite/pull/9253)
* Fix slack oauth scopes, and updated to v2 in [9228](https://github.com/appwrite/appwrite/pull/9228)
* Fix forwarded user agent in [9271](https://github.com/appwrite/appwrite/pull/9271)
* Fix WEBP File Preview Rendering Issue in [9321](https://github.com/appwrite/appwrite/pull/9321)
* Fix build memory specifications in [9360](https://github.com/appwrite/appwrite/pull/9360)
* Fix Self Hosting functions by adding missed config in [9373](https://github.com/appwrite/appwrite/pull/9373)
* Fix resend team invite if already accepted in [9348](https://github.com/appwrite/appwrite/pull/9348)
* Fix null errors on team invite in [9391](https://github.com/appwrite/appwrite/pull/9391)
* Fix email (smtp) to multiple recipients in [9243](https://github.com/appwrite/appwrite/pull/9243)
* Fix stats timing by using receivedAt date when available in [9428](https://github.com/appwrite/appwrite/pull/9428)
* Make min/max params optional for attribute update in [9387](https://github.com/appwrite/appwrite/pull/9387)
* Fix blocking of phone sessions when disabled on console in [9447](https://github.com/appwrite/appwrite/pull/9447)
* Fix logging config in [9467](https://github.com/appwrite/appwrite/pull/9467)
* Update audit timestamp origin in [9481](https://github.com/appwrite/appwrite/pull/9481)
* Fix certificates in deletes worker in [9466](https://github.com/appwrite/appwrite/pull/9466)
* Fix console audits delete in [9547](https://github.com/appwrite/appwrite/pull/9547)
* Fix migrations in [9633](https://github.com/appwrite/appwrite/pull/9633)
* Ensure all 4xx errors in OAuth redirect lead to the failure URL in [9679](https://github.com/appwrite/appwrite/pull/9679)
* Treat 0 as unlimited for CPUs and memory in [9638](https://github.com/appwrite/appwrite/pull/9638)
* Add contextual dispatch logic to fix high CPU usage in [9687](https://github.com/appwrite/appwrite/pull/9687)
* Remove failed attribute in [#9032](https://github.com/appwrite/appwrite/pull/9032)
* Fix delete notFound attribute in [#9038](https://github.com/appwrite/appwrite/pull/9038)
* 🇮🇸 Added missing Icelandic translations for email strings. in [#4848](https://github.com/appwrite/appwrite/pull/4848)
* fix doc comment for filter method in [#5769](https://github.com/appwrite/appwrite/pull/5769)
* Delete attribute No throwing Exception on not found in [#9157](https://github.com/appwrite/appwrite/pull/9157)
* Fix VCS identity collision in [#9138](https://github.com/appwrite/appwrite/pull/9138)
* Fix disabling of email-otp when user wants to in [#9200](https://github.com/appwrite/appwrite/pull/9200)
* Ensure user can delete session in [#9209](https://github.com/appwrite/appwrite/pull/9209)
* Fix resend invitation in [#9218](https://github.com/appwrite/appwrite/pull/9218)
* Fix phone number parsing exception handling in [#9246](https://github.com/appwrite/appwrite/pull/9246)
* Fix amazon oauth in [#9253](https://github.com/appwrite/appwrite/pull/9253)
* Fix slack oauth scopes, and updated to v2 in [#9228](https://github.com/appwrite/appwrite/pull/9228)
* Fix forwarded user agent in [#9271](https://github.com/appwrite/appwrite/pull/9271)
* Fix WEBP File Preview Rendering Issue in [#9321](https://github.com/appwrite/appwrite/pull/9321)
* Fix build memory specifications in [#9360](https://github.com/appwrite/appwrite/pull/9360)
* Fix Self Hosting functions by adding missed config in [#9373](https://github.com/appwrite/appwrite/pull/9373)
* Fix resend team invite if already accepted in [#9348](https://github.com/appwrite/appwrite/pull/9348)
* Fix null errors on team invite in [#9391](https://github.com/appwrite/appwrite/pull/9391)
* Fix email (smtp) to multiple recipients in [#9243](https://github.com/appwrite/appwrite/pull/9243)
* Fix stats timing by using receivedAt date when available in [#9428](https://github.com/appwrite/appwrite/pull/9428)
* Make min/max params optional for attribute update in [#9387](https://github.com/appwrite/appwrite/pull/9387)
* Fix blocking of phone sessions when disabled on console in [#9447](https://github.com/appwrite/appwrite/pull/9447)
* Fix logging config in [#9467](https://github.com/appwrite/appwrite/pull/9467)
* Update audit timestamp origin in [#9481](https://github.com/appwrite/appwrite/pull/9481)
* Fix certificates in deletes worker in [#9466](https://github.com/appwrite/appwrite/pull/9466)
* Fix console audits delete in [#9547](https://github.com/appwrite/appwrite/pull/9547)
* Fix migrations in [#9633](https://github.com/appwrite/appwrite/pull/9633)
* Ensure all 4xx errors in OAuth redirect lead to the failure URL in [#9679](https://github.com/appwrite/appwrite/pull/9679)
* Treat 0 as unlimited for CPUs and memory in [#9638](https://github.com/appwrite/appwrite/pull/9638)
* Add contextual dispatch logic to fix high CPU usage in [#9687](https://github.com/appwrite/appwrite/pull/9687)
### Miscellaneous
* Merge 1.6.x into feat-custom-cf-hostnames in [8904](https://github.com/appwrite/appwrite/pull/8904)
* Improve compression param checks in [8922](https://github.com/appwrite/appwrite/pull/8922)
* upgrade utopia storage in [8930](https://github.com/appwrite/appwrite/pull/8930)
* Feat migration in [8797](https://github.com/appwrite/appwrite/pull/8797)
* feat fix web routes in [8962](https://github.com/appwrite/appwrite/pull/8962)
* Fix no pool access in [9027](https://github.com/appwrite/appwrite/pull/9027)
* feat: use environment variable to check rules format in [9039](https://github.com/appwrite/appwrite/pull/9039)
* Update storage.php in [9037](https://github.com/appwrite/appwrite/pull/9037)
* Upgrade db 0.53.200 in [9050](https://github.com/appwrite/appwrite/pull/9050)
* Chore: upgrade utopia storage in [9066](https://github.com/appwrite/appwrite/pull/9066)
* Update usage-dump payload in [9085](https://github.com/appwrite/appwrite/pull/9085)
* GitHub Workflows security hardening in [3728](https://github.com/appwrite/appwrite/pull/3728)
* Update add-oauth2-provider.md in [4313](https://github.com/appwrite/appwrite/pull/4313)
* update readme-cn some doc in [5278](https://github.com/appwrite/appwrite/pull/5278)
* Add accessibility features in [7042](https://github.com/appwrite/appwrite/pull/7042)
* Add Appwrite Cloud to read me. in [5445](https://github.com/appwrite/appwrite/pull/5445)
* Migration throw error in [9092](https://github.com/appwrite/appwrite/pull/9092)
* Fix usage payload bug in [9097](https://github.com/appwrite/appwrite/pull/9097)
* chore: replace occurrences of dbForConsole to dbForPlatform in [9096](https://github.com/appwrite/appwrite/pull/9096)
* fix(realtime): decrement connectionCounter only if connection is known in [9055](https://github.com/appwrite/appwrite/pull/9055)
* payload bug fix in [9098](https://github.com/appwrite/appwrite/pull/9098)
* Fix usage payload bug in [9099](https://github.com/appwrite/appwrite/pull/9099)
* Usage payload debug in [9101](https://github.com/appwrite/appwrite/pull/9101)
* Usage payload debug in [9103](https://github.com/appwrite/appwrite/pull/9103)
* Usage payload debug in [9104](https://github.com/appwrite/appwrite/pull/9104)
* Feat: createFunction abuse labels in [9102](https://github.com/appwrite/appwrite/pull/9102)
* Docs-create-document in [9105](https://github.com/appwrite/appwrite/pull/9105)
* Docs: Create document and unknown attribute error messages. in [5427](https://github.com/appwrite/appwrite/pull/5427)
* Fix: update project accessed at from router and schedulers in [9109](https://github.com/appwrite/appwrite/pull/9109)
* chore: initial commit in [9111](https://github.com/appwrite/appwrite/pull/9111)
* chore: optimise webhooks payload in [9115](https://github.com/appwrite/appwrite/pull/9115)
* Revert "chore: initial commit" in [9117](https://github.com/appwrite/appwrite/pull/9117)
* chore: fix attribute name in [9118](https://github.com/appwrite/appwrite/pull/9118)
* Migrate to redis abuse in [9124](https://github.com/appwrite/appwrite/pull/9124)
* Added webhooks usage stats in [9125](https://github.com/appwrite/appwrite/pull/9125)
* chore remove abuse cleanup in [9137](https://github.com/appwrite/appwrite/pull/9137)
* fix: remove abuse delete trigger in [9139](https://github.com/appwrite/appwrite/pull/9139)
* Remove firebase OAuth API endpoints in [9144](https://github.com/appwrite/appwrite/pull/9144)
* chore: release client sdks in [9112](https://github.com/appwrite/appwrite/pull/9112)
* Update general.php in [9155](https://github.com/appwrite/appwrite/pull/9155)
* feat(swoole): allow configuration override of available cpus in [9177](https://github.com/appwrite/appwrite/pull/9177)
* Usage databases api read writes addition in [9142](https://github.com/appwrite/appwrite/pull/9142)
* Fix dead connections in [9190](https://github.com/appwrite/appwrite/pull/9190)
* Add hostname to audits in [9165](https://github.com/appwrite/appwrite/pull/9165)
* chore: shifted authphone usage tracking to api calls in [9191](https://github.com/appwrite/appwrite/pull/9191)
* Revert "Fix dead connections" in [9201](https://github.com/appwrite/appwrite/pull/9201)
* Add assertEventually to messaging provider logs test in [9192](https://github.com/appwrite/appwrite/pull/9192)
* feat project sms usage in [9198](https://github.com/appwrite/appwrite/pull/9198)
* chore: add audit labels to project resources in [9056](https://github.com/appwrite/appwrite/pull/9056)
* fix sms usage in [9207](https://github.com/appwrite/appwrite/pull/9207)
* Update database in [9202](https://github.com/appwrite/appwrite/pull/9202)
* Fix dead connections in [9213](https://github.com/appwrite/appwrite/pull/9213)
* Revert "Fix dead connections" in [9214](https://github.com/appwrite/appwrite/pull/9214)
* Add logs db init for consistency in [9163](https://github.com/appwrite/appwrite/pull/9163)
* Split the collection definitions in [9153](https://github.com/appwrite/appwrite/pull/9153)
* Log path with populated parameters in [9220](https://github.com/appwrite/appwrite/pull/9220)
* Add missing scope on function template in [9208](https://github.com/appwrite/appwrite/pull/9208)
* Add relatedCollection default in [9225](https://github.com/appwrite/appwrite/pull/9225)
* fix: function usage in [9235](https://github.com/appwrite/appwrite/pull/9235)
* feat: optimise events payloads in [9232](https://github.com/appwrite/appwrite/pull/9232)
* Optimise webhook events in [9168](https://github.com/appwrite/appwrite/pull/9168)
* fix: maintenance job missing type in [9238](https://github.com/appwrite/appwrite/pull/9238)
* Update Fetch to 0.3.0 in [9245](https://github.com/appwrite/appwrite/pull/9245)
* Fix maintenance job in [9247](https://github.com/appwrite/appwrite/pull/9247)
* chore: add missing case for executions in [9248](https://github.com/appwrite/appwrite/pull/9248)
* Add index dependency exception in [9226](https://github.com/appwrite/appwrite/pull/9226)
* chore: fix benchmarking test when made from fork in [9233](https://github.com/appwrite/appwrite/pull/9233)
* Update SDK Generator versions in [9188](https://github.com/appwrite/appwrite/pull/9188)
* chore: skipped job instead of throwing error in [9250](https://github.com/appwrite/appwrite/pull/9250)
* Implement new SDK Class on 1.6.x in [9237](https://github.com/appwrite/appwrite/pull/9237)
* Delete collection before Appwrite's attributes in [9256](https://github.com/appwrite/appwrite/pull/9256)
* Feat batch usage dump in [9255](https://github.com/appwrite/appwrite/pull/9255)
* Fix cloud tests in [9261](https://github.com/appwrite/appwrite/pull/9261)
* Usage: Databases reads writes in [9260](https://github.com/appwrite/appwrite/pull/9260)
* Update: Latest sdk specs in [9274](https://github.com/appwrite/appwrite/pull/9274)
* Revert "Feat batch usage dump" in [9276](https://github.com/appwrite/appwrite/pull/9276)
* feat: add fast2SMS adapter in [9263](https://github.com/appwrite/appwrite/pull/9263)
* Update Sdk Generator dependency in [9280](https://github.com/appwrite/appwrite/pull/9280)
* Transformed at addition in [9281](https://github.com/appwrite/appwrite/pull/9281)
* Docs: clarify update endpoints only work on draft messages in [9236](https://github.com/appwrite/appwrite/pull/9236)
* Update sdk generator dependency in [9282](https://github.com/appwrite/appwrite/pull/9282)
* Revert "Transformed at addition" in [9284](https://github.com/appwrite/appwrite/pull/9284)
* replaced init for cloud link in [9285](https://github.com/appwrite/appwrite/pull/9285)
* Add transformed at in [9289](https://github.com/appwrite/appwrite/pull/9289)
* Make migrations use Dynamic keys for destination in [9291](https://github.com/appwrite/appwrite/pull/9291)
* Make sessions limit tests assert eventually in [9298](https://github.com/appwrite/appwrite/pull/9298)
* Chore update database in [9306](https://github.com/appwrite/appwrite/pull/9306)
* feat: add AMQP queues in [9287](https://github.com/appwrite/appwrite/pull/9287)
* fix(test): use assertEventually instead of while(true) in [9308](https://github.com/appwrite/appwrite/pull/9308)
* fix(certificate worker): events are published without queue name in [9309](https://github.com/appwrite/appwrite/pull/9309)
* chore: update utopia-php/queue to 0.8.1 in [9311](https://github.com/appwrite/appwrite/pull/9311)
* chore: update utopia-php/queue to 0.8.2 in [9312](https://github.com/appwrite/appwrite/pull/9312)
* fix(schedule-tasks): revert back to direct pool usage in [9313](https://github.com/appwrite/appwrite/pull/9313)
* feat: custom app schemes in [9262](https://github.com/appwrite/appwrite/pull/9262)
* Revert "feat: custom app schemes" in [9319](https://github.com/appwrite/appwrite/pull/9319)
* Restore "feat: custom app schemes"" in [9320](https://github.com/appwrite/appwrite/pull/9320)
* Revert "Restore "feat: custom app schemes""" in [9323](https://github.com/appwrite/appwrite/pull/9323)
* chore: update dependencies in [9330](https://github.com/appwrite/appwrite/pull/9330)
* Feat: logs DB in [9272](https://github.com/appwrite/appwrite/pull/9272)
* Catch invalid index in [9329](https://github.com/appwrite/appwrite/pull/9329)
* Fix: missing call for image transformations counting in [9342](https://github.com/appwrite/appwrite/pull/9342)
* Fix drop abuse on shared table project delete in [9346](https://github.com/appwrite/appwrite/pull/9346)
* Only run all table mode tests on db update in [9338](https://github.com/appwrite/appwrite/pull/9338)
* Fix: missing periodic metric in [9350](https://github.com/appwrite/appwrite/pull/9350)
* feat(builds): check if function is blocked before building in [9332](https://github.com/appwrite/appwrite/pull/9332)
* feat: batch create audit logs in [9347](https://github.com/appwrite/appwrite/pull/9347)
* Chore: Update migrations in [9355](https://github.com/appwrite/appwrite/pull/9355)
* Fix: metric time was not being written to DB in [9354](https://github.com/appwrite/appwrite/pull/9354)
* Fix patch index validation in [9356](https://github.com/appwrite/appwrite/pull/9356)
* Fix image trnasformation metrics in [9370](https://github.com/appwrite/appwrite/pull/9370)
* Use batch delete in worker in [9375](https://github.com/appwrite/appwrite/pull/9375)
* Fix Model Platform is missing response key: store in [9361](https://github.com/appwrite/appwrite/pull/9361)
* Feat key segmented usage in [9336](https://github.com/appwrite/appwrite/pull/9336)
* Feat messaging metrics in [9353](https://github.com/appwrite/appwrite/pull/9353)
* Fix removed audits for shared v2 in [9388](https://github.com/appwrite/appwrite/pull/9388)
* chore: bump utopia-php/image to 0.8.0 in [9390](https://github.com/appwrite/appwrite/pull/9390)
* Fix outdated CLI commands in documentation in [9122](https://github.com/appwrite/appwrite/pull/9122)
* disable logs display in [9398](https://github.com/appwrite/appwrite/pull/9398)
* Log batches per project in [9403](https://github.com/appwrite/appwrite/pull/9403)
* Batch per project in [9410](https://github.com/appwrite/appwrite/pull/9410)
* Fix: stats resources only queue projects accessed in last 3 hours in [9411](https://github.com/appwrite/appwrite/pull/9411)
* Track options requests in [9397](https://github.com/appwrite/appwrite/pull/9397)
* chore: bump docker-base in [9406](https://github.com/appwrite/appwrite/pull/9406)
* refactor: migrate Realtime::send calls to queueForRealtime in [9325](https://github.com/appwrite/appwrite/pull/9325)
* Revert "Fix: stats resources only queue projects accessed in last 3 hours" in [9424](https://github.com/appwrite/appwrite/pull/9424)
* Remove usage and usage dump in favor of stats-usage and stats-usage-dump in [9339](https://github.com/appwrite/appwrite/pull/9339)
* Fix: disable dual writing in [9429](https://github.com/appwrite/appwrite/pull/9429)
* Disable transformedAt update for console users in [9425](https://github.com/appwrite/appwrite/pull/9425)
* chore: add image transformation stats to usage endpoint in [9393](https://github.com/appwrite/appwrite/pull/9393)
* chore: added timeout to deployment builds in tests in [9426](https://github.com/appwrite/appwrite/pull/9426)
* fix: model for image transformations in usage project in [9442](https://github.com/appwrite/appwrite/pull/9442)
* Feat: calculate database storage in stats-resources in [9443](https://github.com/appwrite/appwrite/pull/9443)
* Activities batch writes in [9438](https://github.com/appwrite/appwrite/pull/9438)
* chore: bump cache 0.12.x in [9412](https://github.com/appwrite/appwrite/pull/9412)
* chore: queue console project for maintenance delete in [9479](https://github.com/appwrite/appwrite/pull/9479)
* chore: added logsdb for deletes worker in [9462](https://github.com/appwrite/appwrite/pull/9462)
* Feat: calculate and log time taken for each project in [9491](https://github.com/appwrite/appwrite/pull/9491)
* chore: update initializing dbForLogs in [9494](https://github.com/appwrite/appwrite/pull/9494)
* Feat bulk audit delete in [9487](https://github.com/appwrite/appwrite/pull/9487)
* Prepare 1.6.2 release in [9499](https://github.com/appwrite/appwrite/pull/9499)
* Regenerate specs in [9497](https://github.com/appwrite/appwrite/pull/9497)
* Regenerate examples in [9498](https://github.com/appwrite/appwrite/pull/9498)
* chore: bump sdk in [9414](https://github.com/appwrite/appwrite/pull/9414)
* update queue to 0.9.* in [9505](https://github.com/appwrite/appwrite/pull/9505)
* Feat improve delete queries in [9507](https://github.com/appwrite/appwrite/pull/9507)
* Feat: Add rule attributes in [9508](https://github.com/appwrite/appwrite/pull/9508)
* Sync main into 1.6.x in [9496](https://github.com/appwrite/appwrite/pull/9496)
* Bump console to version 5.2.53 in [9495](https://github.com/appwrite/appwrite/pull/9495)
* Prepare 1.6.1 release in [9294](https://github.com/appwrite/appwrite/pull/9294)
* Improve delete ordering in [9512](https://github.com/appwrite/appwrite/pull/9512)
* Cleanups in [9511](https://github.com/appwrite/appwrite/pull/9511)
* Feat dynamic regions in [9408](https://github.com/appwrite/appwrite/pull/9408)
* Feat env vars to system lib in [9515](https://github.com/appwrite/appwrite/pull/9515)
* Feat: domains count in [9514](https://github.com/appwrite/appwrite/pull/9514)
* Migration read from db in [9529](https://github.com/appwrite/appwrite/pull/9529)
* feat: add pool telemetry in [9530](https://github.com/appwrite/appwrite/pull/9530)
* Disable PDO persistence since we manage our own pool in [9526](https://github.com/appwrite/appwrite/pull/9526)
* chore: set min operations to 1 for reads and writes in [9536](https://github.com/appwrite/appwrite/pull/9536)
* Remove default region in [9430](https://github.com/appwrite/appwrite/pull/9430)
* Use cursor pagination with bigger limit for maintenance project loop in [9546](https://github.com/appwrite/appwrite/pull/9546)
* chore: stop tests on failure in [9525](https://github.com/appwrite/appwrite/pull/9525)
* chore: only update total count for privileged users in [9554](https://github.com/appwrite/appwrite/pull/9554)
* refactor: initialization of audit retention in [9563](https://github.com/appwrite/appwrite/pull/9563)
* Delete worker queries fixes in [9523](https://github.com/appwrite/appwrite/pull/9523)
* Bump database 0.62.x in [9568](https://github.com/appwrite/appwrite/pull/9568)
* Fix: schedules region filtering in [9577](https://github.com/appwrite/appwrite/pull/9577)
* Deletes worker fix selects for pagination in [9578](https://github.com/appwrite/appwrite/pull/9578)
* Add $permissions for delete documents selects in [9579](https://github.com/appwrite/appwrite/pull/9579)
* chore(audits): return queue pre-fetch results in [9533](https://github.com/appwrite/appwrite/pull/9533)
* Revert "chore(audits): return queue pre-fetch results" in [9586](https://github.com/appwrite/appwrite/pull/9586)
* Feat multi tenant insert in [9573](https://github.com/appwrite/appwrite/pull/9573)
* Add order by for cursor in [9588](https://github.com/appwrite/appwrite/pull/9588)
* Feat update fetch in [9592](https://github.com/appwrite/appwrite/pull/9592)
* Fix tenant casting in [9598](https://github.com/appwrite/appwrite/pull/9598)
* Feat update ws in [9602](https://github.com/appwrite/appwrite/pull/9602)
* Update database in [9603](https://github.com/appwrite/appwrite/pull/9603)
* Fix: image transformation cache in [9608](https://github.com/appwrite/appwrite/pull/9608)
* Remove audit payload in [9610](https://github.com/appwrite/appwrite/pull/9610)
* Sample rate from DSN in [9559](https://github.com/appwrite/appwrite/pull/9559)
* Restrict role change for sole org owner in [9615](https://github.com/appwrite/appwrite/pull/9615)
* chore: update php image to 0.8.1 in [9616](https://github.com/appwrite/appwrite/pull/9616)
* feat: refactor executor setup in [9420](https://github.com/appwrite/appwrite/pull/9420)
* chore: update gitpod.yml config in [9561](https://github.com/appwrite/appwrite/pull/9561)
* chore: update dependencies in [9625](https://github.com/appwrite/appwrite/pull/9625)
* Update migrations lib in [9628](https://github.com/appwrite/appwrite/pull/9628)
* feat: cache telemetry in [9624](https://github.com/appwrite/appwrite/pull/9624)
* Bump console to version 5.2.56 in [9631](https://github.com/appwrite/appwrite/pull/9631)
* Multi region support in [8667](https://github.com/appwrite/appwrite/pull/8667)
* Revert "Multi region support" in [9632](https://github.com/appwrite/appwrite/pull/9632)
* Revert "Revert "Multi region support"" in [9636](https://github.com/appwrite/appwrite/pull/9636)
* Fix tasks in [9644](https://github.com/appwrite/appwrite/pull/9644)
* chore: updated the migration version to 8.6 in [9646](https://github.com/appwrite/appwrite/pull/9646)
* Fix: merge the working of StatsUsage and StatsUsageDump in [9585](https://github.com/appwrite/appwrite/pull/9585)
* Update database in [9643](https://github.com/appwrite/appwrite/pull/9643)
* chore: fix error logging for CLI tasks in [9651](https://github.com/appwrite/appwrite/pull/9651)
* fix: usage test assertion in [9653](https://github.com/appwrite/appwrite/pull/9653)
* Fix keys in [9656](https://github.com/appwrite/appwrite/pull/9656)
* Feat: multi tenant dual writing in [9583](https://github.com/appwrite/appwrite/pull/9583)
* Fix/throwing 400 for null order attributes in [9657](https://github.com/appwrite/appwrite/pull/9657)
* feat: sdk group attribute in [9596](https://github.com/appwrite/appwrite/pull/9596)
* Add configurable function and build size in [9648](https://github.com/appwrite/appwrite/pull/9648)
* feat: update API endpoint in the code examples in [8933](https://github.com/appwrite/appwrite/pull/8933)
* chore: abstract token secret hiding to response model in [9574](https://github.com/appwrite/appwrite/pull/9574)
* chore: update sdks in [9655](https://github.com/appwrite/appwrite/pull/9655)
* feat: allow non-critical events to ignore exceptions when enqueuing the event in [9680](https://github.com/appwrite/appwrite/pull/9680)
* Revert "Add configurable function and build size" in [9681](https://github.com/appwrite/appwrite/pull/9681)
* core: introduce endpoint.docs in specs in [9685](https://github.com/appwrite/appwrite/pull/9685)
* fix: remove content-type header from get request specs in [9666](https://github.com/appwrite/appwrite/pull/9666)
* chore: update flutter sdk in [9691](https://github.com/appwrite/appwrite/pull/9691)
* Merge 1.6.x into feat-custom-cf-hostnames in [#8904](https://github.com/appwrite/appwrite/pull/8904)
* Improve compression param checks in [#8922](https://github.com/appwrite/appwrite/pull/8922)
* upgrade utopia storage in [#8930](https://github.com/appwrite/appwrite/pull/8930)
* Feat migration in [#8797](https://github.com/appwrite/appwrite/pull/8797)
* feat fix web routes in [#8962](https://github.com/appwrite/appwrite/pull/8962)
* Fix no pool access in [#9027](https://github.com/appwrite/appwrite/pull/9027)
* feat: use environment variable to check rules format in [#9039](https://github.com/appwrite/appwrite/pull/9039)
* Update storage.php in [#9037](https://github.com/appwrite/appwrite/pull/9037)
* Upgrade db 0.53.200 in [#9050](https://github.com/appwrite/appwrite/pull/9050)
* Chore: upgrade utopia storage in [#9066](https://github.com/appwrite/appwrite/pull/9066)
* Update usage-dump payload in [#9085](https://github.com/appwrite/appwrite/pull/9085)
* GitHub Workflows security hardening in [#3728](https://github.com/appwrite/appwrite/pull/3728)
* Update add-oauth2-provider.md in [#4313](https://github.com/appwrite/appwrite/pull/4313)
* update readme-cn some doc in [#5278](https://github.com/appwrite/appwrite/pull/5278)
* Add accessibility features in [#7042](https://github.com/appwrite/appwrite/pull/7042)
* Add Appwrite Cloud to read me. in [#5445](https://github.com/appwrite/appwrite/pull/5445)
* Migration throw error in [#9092](https://github.com/appwrite/appwrite/pull/9092)
* Fix usage payload bug in [#9097](https://github.com/appwrite/appwrite/pull/9097)
* chore: replace occurrences of dbForConsole to dbForPlatform in [#9096](https://github.com/appwrite/appwrite/pull/9096)
* fix(realtime): decrement connectionCounter only if connection is known in [#9055](https://github.com/appwrite/appwrite/pull/9055)
* payload bug fix in [#9098](https://github.com/appwrite/appwrite/pull/9098)
* Fix usage payload bug in [#9099](https://github.com/appwrite/appwrite/pull/9099)
* Usage payload debug in [#9101](https://github.com/appwrite/appwrite/pull/9101)
* Usage payload debug in [#9103](https://github.com/appwrite/appwrite/pull/9103)
* Usage payload debug in [#9104](https://github.com/appwrite/appwrite/pull/9104)
* Feat: createFunction abuse labels in [#9102](https://github.com/appwrite/appwrite/pull/9102)
* Docs-create-document in [#9105](https://github.com/appwrite/appwrite/pull/9105)
* Docs: Create document and unknown attribute error messages. in [#5427](https://github.com/appwrite/appwrite/pull/5427)
* Fix: update project accessed at from router and schedulers in [#9109](https://github.com/appwrite/appwrite/pull/9109)
* chore: initial commit in [#9111](https://github.com/appwrite/appwrite/pull/9111)
* chore: optimise webhooks payload in [#9115](https://github.com/appwrite/appwrite/pull/9115)
* Revert "chore: initial commit" in [#9117](https://github.com/appwrite/appwrite/pull/9117)
* chore: fix attribute name in [#9118](https://github.com/appwrite/appwrite/pull/9118)
* Migrate to redis abuse in [#9124](https://github.com/appwrite/appwrite/pull/9124)
* Added webhooks usage stats in [#9125](https://github.com/appwrite/appwrite/pull/9125)
* chore remove abuse cleanup in [#9137](https://github.com/appwrite/appwrite/pull/9137)
* fix: remove abuse delete trigger in [#9139](https://github.com/appwrite/appwrite/pull/9139)
* Remove firebase OAuth API endpoints in [#9144](https://github.com/appwrite/appwrite/pull/9144)
* chore: release client sdks in [#9112](https://github.com/appwrite/appwrite/pull/9112)
* Update general.php in [#9155](https://github.com/appwrite/appwrite/pull/9155)
* feat(swoole): allow configuration override of available cpus in [#9177](https://github.com/appwrite/appwrite/pull/9177)
* Usage databases api read writes addition in [#9142](https://github.com/appwrite/appwrite/pull/9142)
* Fix dead connections in [#9190](https://github.com/appwrite/appwrite/pull/9190)
* Add hostname to audits in [#9165](https://github.com/appwrite/appwrite/pull/9165)
* chore: shifted authphone usage tracking to api calls in [#9191](https://github.com/appwrite/appwrite/pull/9191)
* Revert "Fix dead connections" in [#9201](https://github.com/appwrite/appwrite/pull/9201)
* Add assertEventually to messaging provider logs test in [#9192](https://github.com/appwrite/appwrite/pull/9192)
* feat project sms usage in [#9198](https://github.com/appwrite/appwrite/pull/9198)
* chore: add audit labels to project resources in [#9056](https://github.com/appwrite/appwrite/pull/9056)
* fix sms usage in [#9207](https://github.com/appwrite/appwrite/pull/9207)
* Update database in [#9202](https://github.com/appwrite/appwrite/pull/9202)
* Fix dead connections in [#9213](https://github.com/appwrite/appwrite/pull/9213)
* Revert "Fix dead connections" in [#9214](https://github.com/appwrite/appwrite/pull/9214)
* Add logs db init for consistency in [#9163](https://github.com/appwrite/appwrite/pull/9163)
* Split the collection definitions in [#9153](https://github.com/appwrite/appwrite/pull/9153)
* Log path with populated parameters in [#9220](https://github.com/appwrite/appwrite/pull/9220)
* Add missing scope on function template in [#9208](https://github.com/appwrite/appwrite/pull/9208)
* Add relatedCollection default in [#9225](https://github.com/appwrite/appwrite/pull/9225)
* fix: function usage in [#9235](https://github.com/appwrite/appwrite/pull/9235)
* feat: optimise events payloads in [#9232](https://github.com/appwrite/appwrite/pull/9232)
* Optimise webhook events in [#9168](https://github.com/appwrite/appwrite/pull/9168)
* fix: maintenance job missing type in [#9238](https://github.com/appwrite/appwrite/pull/9238)
* Update Fetch to 0.3.0 in [#9245](https://github.com/appwrite/appwrite/pull/9245)
* Fix maintenance job in [#9247](https://github.com/appwrite/appwrite/pull/9247)
* chore: add missing case for executions in [#9248](https://github.com/appwrite/appwrite/pull/9248)
* Add index dependency exception in [#9226](https://github.com/appwrite/appwrite/pull/9226)
* chore: fix benchmarking test when made from fork in [#9233](https://github.com/appwrite/appwrite/pull/9233)
* Update SDK Generator versions in [#9188](https://github.com/appwrite/appwrite/pull/9188)
* chore: skipped job instead of throwing error in [#9250](https://github.com/appwrite/appwrite/pull/9250)
* Implement new SDK Class on 1.6.x in [#9237](https://github.com/appwrite/appwrite/pull/9237)
* Delete collection before Appwrite's attributes in [#9256](https://github.com/appwrite/appwrite/pull/9256)
* Feat batch usage dump in [#9255](https://github.com/appwrite/appwrite/pull/9255)
* Fix cloud tests in [#9261](https://github.com/appwrite/appwrite/pull/9261)
* Usage: Databases reads writes in [#9260](https://github.com/appwrite/appwrite/pull/9260)
* Update: Latest sdk specs in [#9274](https://github.com/appwrite/appwrite/pull/9274)
* Revert "Feat batch usage dump" in [#9276](https://github.com/appwrite/appwrite/pull/9276)
* feat: add fast2SMS adapter in [#9263](https://github.com/appwrite/appwrite/pull/9263)
* Update Sdk Generator dependency in [#9280](https://github.com/appwrite/appwrite/pull/9280)
* Transformed at addition in [#9281](https://github.com/appwrite/appwrite/pull/9281)
* Docs: clarify update endpoints only work on draft messages in [#9236](https://github.com/appwrite/appwrite/pull/9236)
* Update sdk generator dependency in [#9282](https://github.com/appwrite/appwrite/pull/9282)
* Revert "Transformed at addition" in [#9284](https://github.com/appwrite/appwrite/pull/9284)
* replaced init for cloud link in [#9285](https://github.com/appwrite/appwrite/pull/9285)
* Add transformed at in [#9289](https://github.com/appwrite/appwrite/pull/9289)
* Make migrations use Dynamic keys for destination in [#9291](https://github.com/appwrite/appwrite/pull/9291)
* Make sessions limit tests assert eventually in [#9298](https://github.com/appwrite/appwrite/pull/9298)
* Chore update database in [#9306](https://github.com/appwrite/appwrite/pull/9306)
* feat: add AMQP queues in [#9287](https://github.com/appwrite/appwrite/pull/9287)
* fix(test): use assertEventually instead of while(true) in [#9308](https://github.com/appwrite/appwrite/pull/9308)
* fix(certificate worker): events are published without queue name in [#9309](https://github.com/appwrite/appwrite/pull/9309)
* chore: update utopia-php/queue to 0.8.1 in [#9311](https://github.com/appwrite/appwrite/pull/9311)
* chore: update utopia-php/queue to 0.8.2 in [#9312](https://github.com/appwrite/appwrite/pull/9312)
* fix(schedule-tasks): revert back to direct pool usage in [#9313](https://github.com/appwrite/appwrite/pull/9313)
* feat: custom app schemes in [#9262](https://github.com/appwrite/appwrite/pull/9262)
* Revert "feat: custom app schemes" in [#9319](https://github.com/appwrite/appwrite/pull/9319)
* Restore "feat: custom app schemes"" in [#9320](https://github.com/appwrite/appwrite/pull/9320)
* Revert "Restore "feat: custom app schemes""" in [#9323](https://github.com/appwrite/appwrite/pull/9323)
* chore: update dependencies in [#9330](https://github.com/appwrite/appwrite/pull/9330)
* Feat: logs DB in [#9272](https://github.com/appwrite/appwrite/pull/9272)
* Catch invalid index in [#9329](https://github.com/appwrite/appwrite/pull/9329)
* Fix: missing call for image transformations counting in [#9342](https://github.com/appwrite/appwrite/pull/9342)
* Fix drop abuse on shared table project delete in [#9346](https://github.com/appwrite/appwrite/pull/9346)
* Only run all table mode tests on db update in [#9338](https://github.com/appwrite/appwrite/pull/9338)
* Fix: missing periodic metric in [#9350](https://github.com/appwrite/appwrite/pull/9350)
* feat(builds): check if function is blocked before building in [#9332](https://github.com/appwrite/appwrite/pull/9332)
* feat: batch create audit logs in [#9347](https://github.com/appwrite/appwrite/pull/9347)
* Chore: Update migrations in [#9355](https://github.com/appwrite/appwrite/pull/9355)
* Fix: metric time was not being written to DB in [#9354](https://github.com/appwrite/appwrite/pull/9354)
* Fix patch index validation in [#9356](https://github.com/appwrite/appwrite/pull/9356)
* Fix image trnasformation metrics in [#9370](https://github.com/appwrite/appwrite/pull/9370)
* Use batch delete in worker in [#9375](https://github.com/appwrite/appwrite/pull/9375)
* Fix Model Platform is missing response key: store in [#9361](https://github.com/appwrite/appwrite/pull/9361)
* Feat key segmented usage in [#9336](https://github.com/appwrite/appwrite/pull/9336)
* Feat messaging metrics in [#9353](https://github.com/appwrite/appwrite/pull/9353)
* Fix removed audits for shared v2 in [#9388](https://github.com/appwrite/appwrite/pull/9388)
* chore: bump utopia-php/image to 0.8.0 in [#9390](https://github.com/appwrite/appwrite/pull/9390)
* Fix outdated CLI commands in documentation in [#9122](https://github.com/appwrite/appwrite/pull/9122)
* disable logs display in [#9398](https://github.com/appwrite/appwrite/pull/9398)
* Log batches per project in [#9403](https://github.com/appwrite/appwrite/pull/9403)
* Batch per project in [#9410](https://github.com/appwrite/appwrite/pull/9410)
* Fix: stats resources only queue projects accessed in last 3 hours in [#9411](https://github.com/appwrite/appwrite/pull/9411)
* Track options requests in [#9397](https://github.com/appwrite/appwrite/pull/9397)
* chore: bump docker-base in [#9406](https://github.com/appwrite/appwrite/pull/9406)
* refactor: migrate Realtime::send calls to queueForRealtime in [#9325](https://github.com/appwrite/appwrite/pull/9325)
* Revert "Fix: stats resources only queue projects accessed in last 3 hours" in [#9424](https://github.com/appwrite/appwrite/pull/9424)
* Remove usage and usage dump in favor of stats-usage and stats-usage-dump in [#9339](https://github.com/appwrite/appwrite/pull/9339)
* Fix: disable dual writing in [#9429](https://github.com/appwrite/appwrite/pull/9429)
* Disable transformedAt update for console users in [#9425](https://github.com/appwrite/appwrite/pull/9425)
* chore: add image transformation stats to usage endpoint in [#9393](https://github.com/appwrite/appwrite/pull/9393)
* chore: added timeout to deployment builds in tests in [#9426](https://github.com/appwrite/appwrite/pull/9426)
* fix: model for image transformations in usage project in [#9442](https://github.com/appwrite/appwrite/pull/9442)
* Feat: calculate database storage in stats-resources in [#9443](https://github.com/appwrite/appwrite/pull/9443)
* Activities batch writes in [#9438](https://github.com/appwrite/appwrite/pull/9438)
* chore: bump cache 0.12.x in [#9412](https://github.com/appwrite/appwrite/pull/9412)
* chore: queue console project for maintenance delete in [#9479](https://github.com/appwrite/appwrite/pull/9479)
* chore: added logsdb for deletes worker in [#9462](https://github.com/appwrite/appwrite/pull/9462)
* Feat: calculate and log time taken for each project in [#9491](https://github.com/appwrite/appwrite/pull/9491)
* chore: update initializing dbForLogs in [#9494](https://github.com/appwrite/appwrite/pull/9494)
* Feat bulk audit delete in [#9487](https://github.com/appwrite/appwrite/pull/9487)
* Prepare 1.6.2 release in [#9499](https://github.com/appwrite/appwrite/pull/9499)
* Regenerate specs in [#9497](https://github.com/appwrite/appwrite/pull/9497)
* Regenerate examples in [#9498](https://github.com/appwrite/appwrite/pull/9498)
* chore: bump sdk in [#9414](https://github.com/appwrite/appwrite/pull/9414)
* update queue to 0.9.* in [#9505](https://github.com/appwrite/appwrite/pull/9505)
* Feat improve delete queries in [#9507](https://github.com/appwrite/appwrite/pull/9507)
* Feat: Add rule attributes in [#9508](https://github.com/appwrite/appwrite/pull/9508)
* Sync main into 1.6.x in [#9496](https://github.com/appwrite/appwrite/pull/9496)
* Bump console to version 5.2.53 in [#9495](https://github.com/appwrite/appwrite/pull/9495)
* Prepare 1.6.1 release in [#9294](https://github.com/appwrite/appwrite/pull/9294)
* Improve delete ordering in [#9512](https://github.com/appwrite/appwrite/pull/9512)
* Cleanups in [#9511](https://github.com/appwrite/appwrite/pull/9511)
* Feat dynamic regions in [#9408](https://github.com/appwrite/appwrite/pull/9408)
* Feat env vars to system lib in [#9515](https://github.com/appwrite/appwrite/pull/9515)
* Feat: domains count in [#9514](https://github.com/appwrite/appwrite/pull/9514)
* Migration read from db in [#9529](https://github.com/appwrite/appwrite/pull/9529)
* feat: add pool telemetry in [#9530](https://github.com/appwrite/appwrite/pull/9530)
* Disable PDO persistence since we manage our own pool in [#9526](https://github.com/appwrite/appwrite/pull/9526)
* chore: set min operations to 1 for reads and writes in [#9536](https://github.com/appwrite/appwrite/pull/9536)
* Remove default region in [#9430](https://github.com/appwrite/appwrite/pull/9430)
* Use cursor pagination with bigger limit for maintenance project loop in [#9546](https://github.com/appwrite/appwrite/pull/9546)
* chore: stop tests on failure in [#9525](https://github.com/appwrite/appwrite/pull/9525)
* chore: only update total count for privileged users in [#9554](https://github.com/appwrite/appwrite/pull/9554)
* refactor: initialization of audit retention in [#9563](https://github.com/appwrite/appwrite/pull/9563)
* Delete worker queries fixes in [#9523](https://github.com/appwrite/appwrite/pull/9523)
* Bump database 0.62.x in [#9568](https://github.com/appwrite/appwrite/pull/9568)
* Fix: schedules region filtering in [#9577](https://github.com/appwrite/appwrite/pull/9577)
* Deletes worker fix selects for pagination in [#9578](https://github.com/appwrite/appwrite/pull/9578)
* Add $permissions for delete documents selects in [#9579](https://github.com/appwrite/appwrite/pull/9579)
* chore(audits): return queue pre-fetch results in [#9533](https://github.com/appwrite/appwrite/pull/9533)
* Revert "chore(audits): return queue pre-fetch results" in [#9586](https://github.com/appwrite/appwrite/pull/9586)
* Feat multi tenant insert in [#9573](https://github.com/appwrite/appwrite/pull/9573)
* Add order by for cursor in [#9588](https://github.com/appwrite/appwrite/pull/9588)
* Feat update fetch in [#9592](https://github.com/appwrite/appwrite/pull/9592)
* Fix tenant casting in [#9598](https://github.com/appwrite/appwrite/pull/9598)
* Feat update ws in [#9602](https://github.com/appwrite/appwrite/pull/9602)
* Update database in [#9603](https://github.com/appwrite/appwrite/pull/9603)
* Fix: image transformation cache in [#9608](https://github.com/appwrite/appwrite/pull/9608)
* Remove audit payload in [#9610](https://github.com/appwrite/appwrite/pull/9610)
* Sample rate from DSN in [#9559](https://github.com/appwrite/appwrite/pull/9559)
* Restrict role change for sole org owner in [#9615](https://github.com/appwrite/appwrite/pull/9615)
* chore: update php image to 0.8.1 in [#9616](https://github.com/appwrite/appwrite/pull/9616)
* feat: refactor executor setup in [#9420](https://github.com/appwrite/appwrite/pull/9420)
* chore: update gitpod.yml config in [#9561](https://github.com/appwrite/appwrite/pull/9561)
* chore: update dependencies in [#9625](https://github.com/appwrite/appwrite/pull/9625)
* Update migrations lib in [#9628](https://github.com/appwrite/appwrite/pull/9628)
* feat: cache telemetry in [#9624](https://github.com/appwrite/appwrite/pull/9624)
* Bump console to version 5.2.56 in [#9631](https://github.com/appwrite/appwrite/pull/9631)
* Multi region support in [#8667](https://github.com/appwrite/appwrite/pull/8667)
* Revert "Multi region support" in [#9632](https://github.com/appwrite/appwrite/pull/9632)
* Revert "Revert "Multi region support"" in [#9636](https://github.com/appwrite/appwrite/pull/9636)
* Fix tasks in [#9644](https://github.com/appwrite/appwrite/pull/9644)
* chore: updated the migration version to 8.6 in [#9646](https://github.com/appwrite/appwrite/pull/9646)
* Fix: merge the working of StatsUsage and StatsUsageDump in [#9585](https://github.com/appwrite/appwrite/pull/9585)
* Update database in [#9643](https://github.com/appwrite/appwrite/pull/9643)
* chore: fix error logging for CLI tasks in [#9651](https://github.com/appwrite/appwrite/pull/9651)
* fix: usage test assertion in [#9653](https://github.com/appwrite/appwrite/pull/9653)
* Fix keys in [#9656](https://github.com/appwrite/appwrite/pull/9656)
* Feat: multi tenant dual writing in [#9583](https://github.com/appwrite/appwrite/pull/9583)
* Fix/throwing 400 for null order attributes in [#9657](https://github.com/appwrite/appwrite/pull/9657)
* feat: sdk group attribute in [#9596](https://github.com/appwrite/appwrite/pull/9596)
* Add configurable function and build size in [#9648](https://github.com/appwrite/appwrite/pull/9648)
* feat: update API endpoint in the code examples in [#8933](https://github.com/appwrite/appwrite/pull/8933)
* chore: abstract token secret hiding to response model in [#9574](https://github.com/appwrite/appwrite/pull/9574)
* chore: update sdks in [#9655](https://github.com/appwrite/appwrite/pull/9655)
* feat: allow non-critical events to ignore exceptions when enqueuing the event in [#9680](https://github.com/appwrite/appwrite/pull/9680)
* Revert "Add configurable function and build size" in [#9681](https://github.com/appwrite/appwrite/pull/9681)
* core: introduce endpoint.docs in specs in [#9685](https://github.com/appwrite/appwrite/pull/9685)
* fix: remove content-type header from get request specs in [#9666](https://github.com/appwrite/appwrite/pull/9666)
* chore: update flutter sdk in [#9691](https://github.com/appwrite/appwrite/pull/9691)
# Version 1.6.1

View file

@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.4 AS final
FROM appwrite/base:0.10.5 AS final
LABEL maintainer="team@appwrite.io"
@ -24,11 +24,9 @@ ENV _APP_VERSION=$VERSION \
_APP_HOME=https://appwrite.io
RUN \
if [ "$DEBUG" == "true" ]; then \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
fi
RUN apk add libwebp
fi
WORKDIR /usr/src/code
@ -98,6 +96,7 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi

View file

@ -1,4 +1,4 @@
> We just announced Timestamp Overrides for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-timestamp-overrides)
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)

Binary file not shown.

View file

@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
$dbForPlatform->setDocumentType('users', User::class);
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
@ -103,6 +105,11 @@ CLI::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
CLI::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools

View file

@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
@ -173,7 +173,7 @@ return [
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'default' => (new Argon2())->getName(),
'array' => false,
'filters' => [],
],
@ -184,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'default' => (new Argon2())->getOptions(),
'array' => false,
'filters' => ['json'],
],
@ -364,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('emailCanonical'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsFree'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsDisposable'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCorporate'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCanonical'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -1527,6 +1582,17 @@ return [
'required' => true,
'array' => false,
],
[
'$id' => ID::custom('transformations'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'array' => false,
'default' => true,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,

View file

@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
@ -2521,4 +2521,128 @@ return [
],
],
],
'transactions' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('transactions'),
'name' => 'Transactions',
'attributes' => [
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'size' => 16, // pending | committing | committed | failed
'signed' => true,
'required' => false,
'default' => 'pending',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('operations'),
'type' => Database::VAR_INTEGER,
'size' => 0,
'signed' => false,
'required' => true,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
],
],
'transactionLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('transactionLogs'),
'name' => 'Transaction Logs',
'attributes' => [
[
'$id' => ID::custom('transactionInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('databaseInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('collectionInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('documentId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('action'),
'type' => Database::VAR_STRING,
'size' => 32, // create | update | upsert | increment | decrement | delete | bulkCreate | bulkUpdate | bulkUpsert | bulkDelete
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
'size' => 5_000_000, // Allow large payloads for bulk operations
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_transaction'),
'type' => Database::INDEX_KEY,
'attributes' => ['transactionInternalId'],
'lengths' => [],
'orders' => [],
],
],
],
];

View file

@ -4,7 +4,6 @@
* Initializes console project document.
*/
use Appwrite\Auth\Auth;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@ -38,7 +37,7 @@ $console = [
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
@ -49,6 +48,7 @@ $console = [
'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
],
'smtpBaseTemplate' => APP_BRANDED_EMAIL_BASE_TEMPLATE,
];
return $console;

View file

@ -211,6 +211,11 @@ return [
'description' => 'User with the requested ID could not be found.',
'code' => 404,
],
Exception::USER_EMAIL_NOT_FOUND => [
'name' => Exception::USER_EMAIL_NOT_FOUND,
'description' => 'User email could not be found.',
'code' => 400,
],
Exception::USER_EMAIL_ALREADY_EXISTS => [
'name' => Exception::USER_EMAIL_ALREADY_EXISTS,
'description' => 'A user with the same email already exists in the current project.',
@ -312,11 +317,21 @@ return [
'description' => 'OAuth2 provider returned some error.',
'code' => 424,
],
Exception::USER_EMAIL_NOT_VERIFIED => [
'name' => Exception::USER_EMAIL_NOT_VERIFIED,
'description' => 'User email is not verified',
'code' => 400,
],
Exception::USER_EMAIL_ALREADY_VERIFIED => [
'name' => Exception::USER_EMAIL_ALREADY_VERIFIED,
'description' => 'User email is already verified',
'code' => 409,
],
Exception::USER_PHONE_NOT_VERIFIED => [
'name' => Exception::USER_PHONE_NOT_VERIFIED,
'description' => 'User phone is not verified',
'code' => 400,
],
Exception::USER_PHONE_ALREADY_VERIFIED => [
'name' => Exception::USER_PHONE_ALREADY_VERIFIED,
'description' => 'User phone is already verified',
@ -507,6 +522,11 @@ return [
'description' => 'The requested file is not publicly readable.',
'code' => 403,
],
Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED => [
'name' => Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED,
'description' => 'Image transformations are disabled for the requested bucket.',
'code' => 403,
],
/** Tokens */
Exception::TOKEN_NOT_FOUND => [
@ -563,6 +583,11 @@ return [
'description' => 'The requested runtime is either inactive or unsupported. Please check the value of the _APP_FUNCTIONS_RUNTIMES environment variable.',
'code' => 404,
],
Exception::FUNCTION_ALREADY_EXISTS => [
'name' => Exception::FUNCTION_ALREADY_EXISTS,
'description' => 'Function with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::FUNCTION_ENTRYPOINT_MISSING => [
'name' => Exception::FUNCTION_ENTRYPOINT_MISSING,
'description' => 'Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".',
@ -966,6 +991,48 @@ return [
'code' => 409,
],
/** Transactions */
Exception::TRANSACTION_NOT_FOUND => [
'name' => Exception::TRANSACTION_NOT_FOUND,
'description' => 'Transaction with the requested ID could not be found.',
'code' => 404,
],
Exception::TRANSACTION_ALREADY_EXISTS => [
'name' => Exception::TRANSACTION_ALREADY_EXISTS,
'description' => 'Transaction with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TRANSACTION_INVALID => [
'name' => Exception::TRANSACTION_INVALID,
'description' => 'The transaction is invalid. Please check the transaction state and try again.',
'code' => 400,
],
Exception::TRANSACTION_FAILED => [
'name' => Exception::TRANSACTION_FAILED,
'description' => 'The transaction has errored. Please check the transaction data and try again.',
'code' => 400,
],
Exception::TRANSACTION_EXPIRED => [
'name' => Exception::TRANSACTION_EXPIRED,
'description' => 'The transaction has expired. Please create a new transaction and try again.',
'code' => 410,
],
Exception::TRANSACTION_CONFLICT => [
'name' => Exception::TRANSACTION_CONFLICT,
'description' => 'The transaction has a conflict. Please resolve the conflict and try again.',
'code' => 409,
],
Exception::TRANSACTION_LIMIT_EXCEEDED => [
'name' => Exception::TRANSACTION_LIMIT_EXCEEDED,
'description' => 'The maximum number of operations per transaction has been exceeded.',
'code' => 400,
],
Exception::TRANSACTION_NOT_READY => [
'name' => Exception::TRANSACTION_NOT_READY,
'description' => 'The transaction is not ready yet. Please try again later.',
'code' => 400,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
'name' => Exception::PROJECT_NOT_FOUND,

View file

@ -202,6 +202,31 @@ return [
]
]
],
'tanstack-start' => [
'key' => 'tanstack-start',
'name' => 'TanStack Start',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './.output',
'startCommand' => 'bash helpers/tanstack-start/server.sh',
],
'static' => [
'key' => 'static',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist/client',
'startCommand' => 'bash helpers/server.sh',
]
]
],
'remix' => [
'key' => 'remix',
'name' => 'Remix',
@ -248,7 +273,7 @@ return [
'key' => 'flutter',
'name' => 'Flutter',
'screenshotSleep' => 5000,
'buildRuntime' => 'flutter-3.29',
'buildRuntime' => 'flutter-3.35',
'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
'adapters' => [
'static' => [
@ -257,6 +282,7 @@ return [
'installCommand' => 'flutter pub get',
'outputDirectory' => './build/web',
'startCommand' => 'bash helpers/server.sh',
'fallbackFile' => 'index.html'
],
],
],

View file

@ -2,6 +2,38 @@
<html>
<head>
<link rel="preconnect" href="https://assets.appwrite.io/" crossorigin>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style type="text/css">
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: dark ) {
body {
color: #616b7c !important;
background-color: #ffffff !important;
}
a {
color: currentColor !important;
}
a.button {
color: #ffffff !important;
background-color: {{accentColor}} !important;
border-color: {{accentColor}} !important;
}
h1, h2, h3 {
color: #373b4d !important;
}
h4 {
color: #4f5769 !important;
}
p.security-phrase:not(:empty), hr {
border-color: #e8e9f0 !important;
}
}
</style>
<style>
@font-face {
font-family: 'Inter';
@ -37,7 +69,6 @@
font-family: "Inter", sans-serif;
background-color: #ffffff;
margin: 0;
padding: 0;
}
a {
color: currentColor;
@ -98,6 +129,7 @@
color: #ffffff;
border-radius: 8px;
height: 48px;
line-height: 24px;
padding: 12px 20px;
box-sizing: border-box;
cursor: pointer;
@ -131,6 +163,14 @@
.social-icon > img {
margin: auto;
}
p.security-phrase:not(:empty) {
opacity: 0.7;
margin: 0;
padding: 0;
margin-top: 32px;
padding-top: 32px;
border-top: 1px solid #e8e9f0;
}
</style>
</head>
@ -145,8 +185,9 @@
<tr>
<td>
<img
height="32px"
height="26px"
src="{{logoUrl}}"
alt="Appwrite logo"
/>
</td>
</tr>
@ -155,12 +196,12 @@
<table style="margin-top: 32px">
<tr>
<td>
<h1>{{subject}}</h1>
<h1>{{heading}}</h1>
</td>
</tr>
</table>
<table style="margin-top: 32px">
<table style="margin-top: 16px">
<tr>
<td>
{{body}}

View file

@ -2,6 +2,38 @@
<html>
<head>
<link rel="preconnect" href="https://assets.appwrite.io/" crossorigin>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style type="text/css">
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: dark ) {
body {
color: #616b7c !important;
background-color: #ffffff !important;
}
a {
color: currentColor !important;
}
a.button {
color: #ffffff !important;
background-color: #2D2D31 !important;
border-color: #414146 !important;
}
h1, h2, h3 {
color: #373b4d !important;
}
h4 {
color: #4f5769 !important;
}
p.security-phrase:not(:empty), hr {
border-color: #e8e9f0 !important;
}
}
</style>
<style>
@font-face {
font-family: 'Inter';
@ -44,6 +76,21 @@
color: currentColor;
word-break: break-all;
}
a.button {
box-sizing: border-box;
display: inline-block;
text-align: center;
text-decoration: none;
padding: 9px 14px;
color: #ffffff;
background-color: #2D2D31;
border: 1px solid #414146;
border-radius: 8px;
}
a.button:hover,
a.button:focus {
opacity: 0.8;
}
table {
width: 100%;
border-spacing: 0 !important;
@ -94,10 +141,15 @@
h* {
font-family: 'Poppins', sans-serif;
}
p {
margin-bottom: 10px;
}
p.security-phrase:not(:empty) {
opacity: 0.7;
margin-top: 32px;
padding-top: 32px;
border-top: 1px solid #e8e9f0;
}
</style>
</head>

View file

@ -0,0 +1,8 @@
<p>{{hello}}</p>
<p>{{body}}</p>
<p>{{footer}}</p>
<p style="margin-bottom: 32px">
{{thanks}}
<br/>
{{signature}}
</p>

View file

@ -1,6 +1,6 @@
<p>{{hello}}</p>
<p>{{body}}</p>
<p><a href="{{redirect}}" target="_blank" style="font-size: 14px; font-family: Inter, sans-serif; color: #ffffff; text-decoration: none; background-color: #2D2D31; border-radius: 8px; padding: 9px 14px; border: 1px solid #414146; display: inline-block; text-align:center; box-sizing: border-box;">{{buttonText}}</a></p>
<p><a href="{{redirect}}" target="_blank" class="button">{{buttonText}}</a></p>
<p>{{footer}}</p>
<p style="margin-bottom: 32px">
{{thanks}}

View file

@ -5,7 +5,7 @@
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; display: inline-block;">
<tr>
<td align="center" style="border-radius: 8px; background-color: #19191D;">
<a rel="noopener" target="_blank" href="{{redirect}}" style="font-size: 14px; font-family: Inter; color: #ffffff; text-decoration: none; border-radius: 8px; padding: 9px 14px; border: 1px solid #19191D; display: inline-block;">{{buttonText}}</a>
<a rel="noopener" target="_blank" href="{{redirect}}" class="button">{{buttonText}}</a>
</td>
</tr>
</table>

View file

@ -5,7 +5,7 @@
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; display: inline-block;">
<tr>
<td align="center" style="border-radius: 8px; background-color: #ffffff;">
<p style="font-size: 24px; text-indent: 18px; letter-spacing: 18px; font-family: 'Inter', sans-serif; color: #414146; text-decoration: none; border-radius: 8px; padding: 24px 12px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold; ">{{otp}}</p>
<p style="font-size: 24px; text-indent: 18px; letter-spacing: 18px; font-family: 'Inter', sans-serif; color: #414146; text-decoration: none; border-radius: 8px; margin-top: 0px; margin-bottom: 0px; padding: 24px 12px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold;">{{otp}}</p>
</td>
</tr>
</table>

View file

@ -5,7 +5,7 @@
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; display: inline-block;">
<tr>
<td align="center" style="border-radius: 8px; background-color: #ffffff;">
<p style="font-size: 24px; text-indent: 18px; letter-spacing: 18px; font-family: Inter; color: #414146; text-decoration: none; border-radius: 8px; padding: 24px 12px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold; ">{{otp}}</p>
<p style="font-size: 24px; text-indent: 18px; letter-spacing: 18px; font-family: 'Inter', sans-serif; color: #414146; text-decoration: none; border-radius: 8px; margin-top: 0px; margin-bottom: 0px; padding: 24px 12px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold; ">{{otp}}</p>
</td>
</tr>
</table>
@ -15,6 +15,4 @@
<p style="margin-bottom: 0px;">{{thanks}}</p>
<p style="margin-top: 0px;">{{signature}}</p>
<hr style="margin-block-start: 1rem; margin-block-end: 1rem;">
<p style="opacity: 0.7;">{{securityPhrase}}</p>
<p class="security-phrase">{{securityPhrase}}</p>

View file

@ -3,8 +3,9 @@
"settings.locale": "en",
"settings.direction": "ltr",
"emails.sender": "{{project}} Team",
"emails.verification.subject": "Account Verification",
"emails.verification.subject": "Account Verification for {{project}}",
"emails.verification.preview": "Verify your email to activate your {{project}} account.",
"emails.verification.heading": "Verify your email to start using {{project}}",
"emails.verification.hello": "Hello {{user}},",
"emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.",
"emails.verification.footer": "If you didnt ask to verify this address, you can ignore this message.",
@ -33,6 +34,7 @@
"emails.sessionAlert.signature": "{{project}} team",
"emails.otpSession.subject": "OTP for {{project}} Login",
"emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.",
"emails.otpSession.heading": "Login with OTP to use {{project}}",
"emails.otpSession.hello": "Hello {{user}},",
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.",
"emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.",
@ -41,12 +43,13 @@
"emails.otpSession.signature": "{{project}} team",
"emails.mfaChallenge.subject": "Verification Code for {{project}}",
"emails.mfaChallenge.preview": "Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.",
"emails.mfaChallenge.heading": "Complete two-step verification to use {{project}}",
"emails.mfaChallenge.hello": "Hello {{user}},",
"emails.mfaChallenge.description": "Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.",
"emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.",
"emails.mfaChallenge.thanks": "Thanks,",
"emails.mfaChallenge.signature": "{{project}} team",
"emails.recovery.subject": "Password Reset",
"emails.recovery.subject": "Password Reset for {{project}}",
"emails.recovery.preview": "Reset your {{project}} password using the link.",
"emails.recovery.hello": "Hello {{user}},",
"emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.",
@ -54,6 +57,21 @@
"emails.recovery.thanks": "Thanks,",
"emails.recovery.buttonText": "Reset password",
"emails.recovery.signature": "{{project}} team",
"emails.csvExport.success.subject": "Your CSV export is ready",
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
"emails.csvExport.success.hello": "Hello {{user}},",
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
"emails.csvExport.success.thanks": "Thanks,",
"emails.csvExport.success.buttonText": "Download CSV",
"emails.csvExport.success.signature": "Appwrite team",
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
"emails.csvExport.failure.hello": "Hello {{user}},",
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
"emails.csvExport.failure.thanks": "Thanks,",
"emails.csvExport.failure.signature": "{{project}} team",
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
"emails.invitation.hello": "Hello {{user}},",

View file

@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '20.1.0',
'version' => '21.5.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '19.1.0',
'version' => '20.3.2',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@ -79,7 +79,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '12.1.0',
'version' => '13.5.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '10.1.0',
'version' => '11.4.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@ -139,7 +139,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.14.0',
'version' => '0.19.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@ -207,7 +207,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '0.1.0',
'version' => '0.2.0',
'url' => '',
'package' => '',
'enabled' => true,
@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '10.0.0',
'version' => '12.0.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@ -262,7 +262,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '19.1.0',
'version' => '21.0.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '17.1.0',
'version' => '19.0.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@ -300,7 +300,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '13.1.0',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@ -319,7 +319,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '18.1.0',
'version' => '20.0.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@ -338,7 +338,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => '0.12.0',
'version' => 'v0.15.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
@ -357,7 +357,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.18.0',
'version' => '0.23.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@ -376,7 +376,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '18.1.0',
'version' => '20.0.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@ -395,7 +395,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '11.1.0',
'version' => '13.0.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@ -418,7 +418,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '12.1.0',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,

View file

@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Utopia\Database\Documents\User;
$member = [
'global',
@ -28,7 +28,7 @@ $member = [
'subscribers.write',
'subscribers.read',
'assistant.read',
'rules.read'
'rules.read',
];
$admins = [
@ -92,7 +92,7 @@ $admins = [
];
return [
Auth::USER_ROLE_GUESTS => [
User::ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'global',
@ -112,23 +112,23 @@ return [
'execution.write',
],
],
Auth::USER_ROLE_USERS => [
User::ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member),
],
Auth::USER_ROLE_ADMIN => [
User::ROLE_ADMIN => [
'label' => 'Admin',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_DEVELOPER => [
User::ROLE_DEVELOPER => [
'label' => 'Developer',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_OWNER => [
User::ROLE_OWNER => [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
Auth::USER_ROLE_APPS => [
User::ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],

View file

@ -47,10 +47,10 @@ return [ // List of publicly visible scopes
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database collection\'s indexes',
'description' => 'Access to read your project\'s database table\'s indexes',
],
'indexes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes',
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
],
'documents.read' => [
'description' => 'Access to read your project\'s database documents',

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ return [
],
'DART' => [
'name' => 'dart',
'versions' => ['3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
'versions' => ['3.9', '3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
@ -38,6 +38,6 @@ return [
],
'FLUTTER' => [
'name' => 'flutter',
'versions' => ['3.32', '3.24']
'versions' => ['3.35', '3.32', '3.24']
],
];

View file

@ -9,6 +9,11 @@ use Utopia\System\System;
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN', '');
// Temporary fix until we can set _APP_DOMAIN to "localhost" instead of "traefik"
if (System::getEnv('_APP_ENV', 'development') === 'development') {
$hostname = 'localhost';
}
$url = $protocol . '://' . $hostname;
class UseCases
@ -19,6 +24,7 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
}
const TEMPLATE_FRAMEWORKS = [
@ -78,7 +84,7 @@ const TEMPLATE_FRAMEWORKS = [
'installCommand' => '',
'buildCommand' => 'flutter build web',
'outputDirectory' => './build/web',
'buildRuntime' => 'flutter-3.29',
'buildRuntime' => 'flutter-3.35',
'adapter' => 'static',
'fallbackFile' => '',
],
@ -111,6 +117,16 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './dist',
'fallbackFile' => '+not-found.html',
],
'TANSTACK_START' => [
'key' => 'tanstack-start',
'name' => 'TanStack Start',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './dist',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => '',
],
'ANGULAR' => [
'key' => 'angular',
'name' => 'Angular',
@ -950,6 +966,50 @@ return [
],
]
],
[
'key' => 'starter-for-tanstack-start',
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-tanstack-start-light.png',
'frameworks' => [
getFramework('TANSTACK_START', [
'providerRootDirectory' => './',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'starter-for-tanstack-start',
'providerOwner' => 'appwrite',
'providerVersion' => '0.1.*',
'variables' => [
[
'name' => 'VITE_APPWRITE_ENDPOINT',
'description' => 'Endpoint of Appwrite server',
'value' => '{apiEndpoint}',
'placeholder' => '{apiEndpoint}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_ID',
'description' => 'Your Appwrite project ID',
'value' => '{projectId}',
'placeholder' => '{projectId}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_NAME',
'description' => 'Your Appwrite project name',
'value' => '{projectName}',
'placeholder' => '{projectName}',
'required' => true,
'type' => 'text'
],
]
],
[
'key' => 'starter-for-nuxt',
'name' => 'Nuxt starter',
@ -1327,6 +1387,25 @@ return [
'providerVersion' => '0.3.*',
'variables' => [],
],
[
'key' => 'playground-for-tanstack-start',
'name' => 'TanStack Start playground',
'tagline' => 'A basic TanStack Start website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-tanstack-start-light.png',
'frameworks' => [
getFramework('TANSTACK_START', [
'providerRootDirectory' => './tanstack-start/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.5.*',
'variables' => [],
],
[
'key' => 'playground-for-react-native',
'name' => 'React Native playground',
@ -1365,4 +1444,32 @@ return [
'providerVersion' => '0.3.*',
'variables' => []
],
[
'key' => 'text-to-speech',
'name' => 'Text-to-speech with ElevenLabs',
'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::AI],
'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/text-to-speech',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.6.*',
'variables' => [
[
'name' => 'ELEVENLABS_API_KEY',
'description' => 'Your ElevenLabs API key',
'value' => '',
'placeholder' => 'sk_.....',
'required' => true,
'type' => 'password'
],
]
],
];

View file

@ -952,6 +952,16 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_BROWSER_HOST',
'description' => 'The host used by Appwrite to communicate with the browser service for screenshots.',
'introduction' => '1.8.0',
'default' => 'http://appwrite-browser:3000/v1',
'required' => false,
'overwrite' => true,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_EXECUTOR_RUNTIME_NETWORK',
'description' => 'Deprecated with 0.14.0, use \'OPEN_RUNTIMES_NETWORK\' instead.',

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@ -23,6 +24,8 @@ use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Logger\Logger;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
@ -93,8 +96,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
$oAuthProviders = Config::getParam('oAuthProviders');
$className = $oAuthProviders[$provider]['class'];
if (!\class_exists($className)) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
@ -635,6 +638,187 @@ App::get('/v1/avatars/initials')
->file($image->getImageBlob());
});
App::get('/v1/avatars/screenshots')
->desc('Get webpage screenshot')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED)
->label('abuse-limit', 60)
->label('cache', true)
->label('cache.resourceType', 'avatar/screenshot')
->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getScreenshot',
description: '/docs/references/avatars/get-screenshot.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE_PNG
))
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com')
->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}')
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920')
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080')
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2')
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark')
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15')
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true')
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749')
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194')
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100')
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true')
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]')
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3')
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800')
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600')
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
->inject('response')
->inject('queueForStatsUsage')
->action(function (string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
if (!$domain->isKnown()) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$client = new Client();
$client->setTimeout(30);
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
// Convert indexed array to empty array (should not happen due to Assoc validator)
if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
$headers = [];
}
// Create a new object to ensure proper JSON serialization
$headersObject = new \stdClass();
foreach ($headers as $key => $value) {
$headersObject->$key = $value;
}
// Create the config with headers as an object
// The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale
$config = [
'url' => $url,
'theme' => $theme,
'headers' => $headersObject,
'sleep' => $sleep * 1000, // Convert seconds to milliseconds
'waitUntil' => 'load',
'viewport' => [
'width' => $viewportWidth,
'height' => $viewportHeight
]
];
// Add scale if not default
if ($scale != 1) {
$config['deviceScaleFactor'] = $scale;
}
// Add optional parameters that were set, preserving arrays as arrays
if (!empty($userAgent)) {
$config['userAgent'] = $userAgent;
}
if ($fullpage) {
$config['fullPage'] = true;
}
if (!empty($locale)) {
$config['locale'] = $locale;
}
if (!empty($timezone)) {
$config['timezoneId'] = $timezone;
}
// Add geolocation if any coordinates are provided
if ($latitude != 0 || $longitude != 0) {
$config['geolocation'] = [
'latitude' => $latitude,
'longitude' => $longitude,
'accuracy' => $accuracy
];
}
if ($touch) {
$config['hasTouch'] = true;
}
// Add permissions if provided (preserve as array)
if (!empty($permissions)) {
$config['permissions'] = $permissions; // Keep as array
}
try {
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$fetchResponse = $client->fetch(
url: $browserEndpoint . '/screenshots',
method: 'POST',
body: $config
);
if ($fetchResponse->getStatusCode() >= 400) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody());
}
$screenshot = $fetchResponse->getBody();
if (empty($screenshot)) {
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated');
}
// Determine if image processing is needed
$needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output);
if ($needsProcessing) {
// Process image with cropping, quality adjustment, or format conversion
$image = new Image($screenshot);
$image->crop($width, $height);
$output = $output ?: 'png'; // Default to PNG if not specified
$resizedScreenshot = $image->output($output, $quality);
unset($image);
} else {
// Return original screenshot without processing
$resizedScreenshot = $screenshot;
$output = 'png'; // Screenshots are typically PNG by default
}
// Set content type based on output format
$outputs = Config::getParam('storage-outputs');
$contentType = $outputs[$output] ?? $outputs['png'];
$queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType($contentType)
->file($resizedScreenshot);
} catch (\Throwable $th) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
}
});
App::get('/v1/cards/cloud')
->desc('Get front Of Cloud Card')
->groups(['api', 'avatars'])

View file

@ -74,6 +74,7 @@ App::get('/v1/console/variables')
// Combine CAA domain with most common flags and tag (no parameters)
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_BUILD_TIMEOUT' => +System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,

View file

@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\GraphQL\Promises\Adapter;
@ -9,6 +8,7 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
@ -32,7 +32,7 @@ App::init()
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}

View file

@ -37,7 +37,6 @@ App::get('/v1/locale')
$currencies = Config::getParam('locale-currencies');
$output = [];
$ip = $request->getIP();
$time = (60 * 60 * 24 * 45); // 45 days cache
$output['ip'] = $ip;
@ -68,10 +67,6 @@ App::get('/v1/locale')
$output['currency'] = $currency;
}
$response
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
;
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});

View file

@ -49,6 +49,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -80,12 +81,12 @@ App::post('/v1/messaging/providers/mailgun')
->param('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -177,7 +178,7 @@ App::post('/v1/messaging/providers/sendgrid')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -231,6 +232,88 @@ App::post('/v1/messaging/providers/sendgrid')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/resend')
->desc('Create Resend provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
->label('audits.resource', 'provider/{response.$id}')
->label('event', 'providers.[providerId].create')
->label('scope', 'providers.write')
->label('resourceType', RESOURCE_TYPE_PROVIDERS)
->label('sdk', new Method(
namespace: 'messaging',
group: 'providers',
name: 'createResendProvider',
description: '/docs/references/messaging/create-resend-provider.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROVIDER,
)
]
))
->param('providerId', '', new CustomId(), 'Provider 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('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
$credentials = [];
if (!empty($apiKey)) {
$credentials['apiKey'] = $apiKey;
}
$options = [
'fromName' => $fromName,
'fromEmail' => $fromEmail,
'replyToName' => $replyToName,
'replyToEmail' => $replyToEmail,
];
if (
$enabled === true
&& !empty($fromEmail)
&& \array_key_exists('apiKey', $credentials)
) {
$enabled = true;
} else {
$enabled = false;
}
$provider = new Document([
'$id' => $providerId,
'name' => $name,
'provider' => 'resend',
'type' => MESSAGE_TYPE_EMAIL,
'enabled' => $enabled,
'credentials' => $credentials,
'options' => $options,
]);
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
}
$queueForEvents
->setParam('providerId', $provider->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/smtp')
->desc('Create SMTP provider')
->groups(['api', 'messaging'])
@ -284,7 +367,7 @@ App::post('/v1/messaging/providers/smtp')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -371,7 +454,7 @@ App::post('/v1/messaging/providers/msg91')
->param('templateId', '', new Text(0), 'Msg91 template ID', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -454,7 +537,7 @@ App::post('/v1/messaging/providers/telesign')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -538,7 +621,7 @@ App::post('/v1/messaging/providers/textmagic')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -622,7 +705,7 @@ App::post('/v1/messaging/providers/twilio')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -706,7 +789,7 @@ App::post('/v1/messaging/providers/vonage')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -806,8 +889,8 @@ App::post('/v1/messaging/providers/fcm')
])
->param('providerId', '', new CustomId(), 'Provider 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('name', '', new Text(128), 'Provider name.')
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -900,7 +983,7 @@ App::post('/v1/messaging/providers/apns')
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -985,9 +1068,10 @@ App::get('/v1/messaging/providers')
))
->param('queries', [], new Providers(), '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(', ', Providers::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('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -1023,7 +1107,7 @@ App::get('/v1/messaging/providers')
}
try {
$providers = $dbForProject->find('providers', $queries);
$total = $dbForProject->count('providers', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('providers', $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.");
}
@ -1053,11 +1137,12 @@ App::get('/v1/messaging/providers/:providerId/logs')
))
->param('providerId', '', new UID(), 'Provider 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 $providerId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@ -1125,7 +1210,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@ -1186,8 +1271,8 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->param('name', '', new Text(128), 'Provider name.', true)
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true)
@ -1297,7 +1382,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@ -1372,6 +1457,104 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/resend/:providerId')
->desc('Update Resend provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
->label('audits.resource', 'provider/{response.$id}')
->label('event', 'providers.[providerId].update')
->label('scope', 'providers.write')
->label('resourceType', RESOURCE_TYPE_PROVIDERS)
->label('sdk', new Method(
namespace: 'messaging',
group: 'providers',
name: 'updateResendProvider',
description: '/docs/references/messaging/update-resend-provider.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER,
)
]
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
$providerAttr = $provider->getAttribute('provider');
if ($providerAttr !== 'resend') {
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
if (!empty($name)) {
$provider->setAttribute('name', $name);
}
$options = $provider->getAttribute('options');
if (!empty($fromName)) {
$options['fromName'] = $fromName;
}
if (!empty($fromEmail)) {
$options['fromEmail'] = $fromEmail;
}
if (!empty($replyToName)) {
$options['replyToName'] = $replyToName;
}
if (!empty($replyToEmail)) {
$options['replyToEmail'] = $replyToEmail;
}
$provider->setAttribute('options', $options);
if (!empty($apiKey)) {
$provider->setAttribute('credentials', [
'apiKey' => $apiKey,
]);
}
if (!\is_null($enabled)) {
if ($enabled) {
if (
\array_key_exists('apiKey', $provider->getAttribute('credentials')) &&
\array_key_exists('fromEmail', $provider->getAttribute('options'))
) {
$provider->setAttribute('enabled', true);
} else {
throw new Exception(Exception::PROVIDER_MISSING_CREDENTIALS);
}
} else {
$provider->setAttribute('enabled', false);
}
}
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
$queueForEvents
->setParam('providerId', $provider->getId());
$response
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/smtp/:providerId')
->desc('Update SMTP provider')
->groups(['api', 'messaging'])
@ -1415,17 +1598,17 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host such as `smtp1.example.com:25;smtp2.example.com`. You can also specify encryption type, for example: `tls://smtp1.example.com:587;ssl://smtp2.example.com:465"`. Hosts will be tried in order.', true)
->param('port', null, new Range(1, 65535), 'SMTP port.', true)
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -1543,7 +1726,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('templateId', '', new Text(0), 'Msg91 template ID.', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
@ -1630,7 +1813,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@ -1719,7 +1902,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@ -1808,7 +1991,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@ -1897,7 +2080,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@ -2005,8 +2188,8 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -2100,12 +2283,12 @@ App::patch('/v1/messaging/providers/apns/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('authKey', '', new Text(0), 'APNS authentication key.', true)
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true)
->param('sandbox', null, new Nullable(new Boolean()), 'Use APNS sandbox environment.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -2292,9 +2475,10 @@ App::get('/v1/messaging/topics')
))
->param('queries', [], new Topics(), '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(', ', Topics::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('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -2330,7 +2514,7 @@ App::get('/v1/messaging/topics')
}
try {
$topics = $dbForProject->find('topics', $queries);
$total = $dbForProject->count('topics', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('topics', $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.");
}
@ -2360,11 +2544,12 @@ App::get('/v1/messaging/topics/:topicId/logs')
))
->param('topicId', '', new UID(), 'Topic 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 $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@ -2433,7 +2618,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@ -2492,8 +2677,8 @@ App::patch('/v1/messaging/topics/:topicId')
]
))
->param('topicId', '', new UID(), 'Topic ID.')
->param('name', null, new Text(128), 'Topic Name.', true)
->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('name', null, new Nullable(new Text(128)), 'Topic Name.', true)
->param('subscribe', null, new Nullable(new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@ -2693,9 +2878,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('queries', [], new Subscribers(), '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(', ', Providers::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('dbForProject')
->inject('response')
->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) {
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -2757,7 +2943,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$response
->dynamic(new Document([
'subscribers' => $subscribers,
'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT),
'total' => $includeTotal ? $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_SUBSCRIBER_LIST);
});
@ -2781,11 +2967,12 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
))
->param('subscriberId', '', new UID(), 'Subscriber 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 $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
if ($subscriber->isEmpty()) {
@ -2854,7 +3041,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@ -3004,7 +3191,7 @@ App::post('/v1/messaging/messages/email')
->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@ -3177,7 +3364,7 @@ App::post('/v1/messaging/messages/sms')
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@ -3300,7 +3487,7 @@ App::post('/v1/messaging/messages/push')
->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('data', null, new JSON(), 'Additional key-value pair data for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional key-value pair data for push notification.', true)
->param('action', '', new Text(256), 'Action for push notification.', true)
->param('image', '', new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
@ -3309,7 +3496,7 @@ App::post('/v1/messaging/messages/push')
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', -1, new Integer(), 'Badge for push notification. Available only for iOS Platform.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
@ -3511,9 +3698,10 @@ App::get('/v1/messaging/messages')
))
->param('queries', [], new Messages(), '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(', ', Messages::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('dbForProject')
->inject('response')
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -3549,7 +3737,7 @@ App::get('/v1/messaging/messages')
}
try {
$messages = $dbForProject->find('messages', $queries);
$total = $dbForProject->count('messages', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('messages', $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.");
}
@ -3579,11 +3767,12 @@ App::get('/v1/messaging/messages/:messageId/logs')
))
->param('messageId', '', new UID(), 'Message 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 $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@ -3652,7 +3841,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@ -3677,9 +3866,10 @@ App::get('/v1/messaging/messages/:messageId/targets')
))
->param('messageId', '', new UID(), 'Message 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 $messageId, array $queries, Response $response, Database $dbForProject) {
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@ -3729,7 +3919,7 @@ App::get('/v1/messaging/messages/:messageId/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.");
}
@ -3792,17 +3982,17 @@ App::patch('/v1/messaging/messages/email/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('subject', null, new Text(998), 'Email Subject.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('html', null, new Boolean(), 'Is content of type HTML', true)
->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('subject', null, new Nullable(new Text(998)), 'Email Subject.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('html', null, new Nullable(new Boolean()), 'Is content of type HTML', true)
->param('cc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new Nullable(new ArrayList(new CompoundUID())), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@ -4018,12 +4208,12 @@ App::patch('/v1/messaging/messages/sms/:messageId')
)
])
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@ -4180,24 +4370,24 @@ App::patch('/v1/messaging/messages/push/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('title', null, new Text(256), 'Title for push notification.', true)
->param('body', null, new Text(64230), 'Body for push notification.', true)
->param('data', null, new JSON(), 'Additional Data for push notification.', true)
->param('action', null, new Text(256), 'Action for push notification.', true)
->param('image', null, new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('title', null, new Nullable(new Text(256)), 'Title for push notification.', true)
->param('body', null, new Nullable(new Text(64230)), 'Body for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional Data for push notification.', true)
->param('action', null, new Nullable(new Text(256)), 'Action for push notification.', true)
->param('image', null, new Nullable(new CompoundUID()), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Nullable(new Text(256)), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Nullable(new Text(256)), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Nullable(new Text(256)), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Nullable(new Text(256)), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Nullable(new Integer()), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')

View file

@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
@ -20,6 +19,7 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries\Documents;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Migration\Resource;
@ -307,7 +307,8 @@ App::post('/v1/migrations/nhost')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/csv')
App::post('/v1/migrations/csv/imports')
->alias('/v1/migrations/csv')
->groups(['api', 'migrations'])
->desc('Import documents from a CSV')
->label('scope', 'migrations.write')
@ -316,8 +317,8 @@ App::post('/v1/migrations/csv')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCsvMigration',
description: '/docs/references/migrations/migration-csv.md',
name: 'createCSVImport',
description: '/docs/references/migrations/migration-csv-import.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@ -335,15 +336,23 @@ App::post('/v1/migrations/csv')
->inject('dbForPlatform')
->inject('project')
->inject('deviceForFiles')
->inject('deviceForImports')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (string $bucketId, string $fileId, string $resourceId, bool $internalFile, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Device $deviceForFiles, Device $deviceForImports, Event $queueForEvents, Migration $queueForMigrations) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($internalFile && !$isPrivilegedUser) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
->action(function (
string $bucketId,
string $fileId,
string $resourceId,
bool $internalFile,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
) {
$bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default');
@ -351,7 +360,7 @@ App::post('/v1/migrations/csv')
return $dbForProject->getDocument('buckets', $bucketId);
});
if ($bucket->isEmpty() || (!$isAPIKey && !$isPrivilegedUser)) {
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@ -365,18 +374,17 @@ App::post('/v1/migrations/csv')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
// no encryption, compression on files above 20MB.
// No encryption or compression on files above 20MB.
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
$compression = $file->getAttribute('algorithm', Compression::NONE);
$hasCompression = $compression !== Compression::NONE;
$migrationId = ID::unique();
$newPath = $deviceForImports->getPath($migrationId . '_' . $fileId . '.csv');
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.csv');
if ($hasEncryption || $hasCompression) {
$source = $deviceForFiles->read($path);
// 1. decrypt
if ($hasEncryption) {
$source = OpenSSL::decrypt(
$source,
@ -388,7 +396,6 @@ App::post('/v1/migrations/csv')
);
}
// 2. decompress
if ($hasCompression) {
switch ($compression) {
case Compression::ZSTD:
@ -400,15 +407,15 @@ App::post('/v1/migrations/csv')
}
}
// manual write after decryption and/or decompression
if (! $deviceForImports->write($newPath, $source, 'text/csv')) {
throw new \Exception("Unable to copy file");
// Manual write after decryption and/or decompression
if (!$deviceForMigrations->write($newPath, $source, 'text/csv')) {
throw new \Exception('Unable to copy file');
}
} elseif (! $deviceForFiles->transfer($path, $newPath, $deviceForImports)) {
throw new \Exception("Unable to copy file");
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
throw new \Exception('Unable to copy file');
}
$fileSize = $deviceForImports->getFileSize($newPath);
$fileSize = $deviceForMigrations->getFileSize($newPath);
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
$migration = $dbForProject->createDocument('migrations', new Document([
@ -441,6 +448,136 @@ App::post('/v1/migrations/csv')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/csv/exports')
->groups(['api', 'migrations'])
->desc('Export documents to CSV')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCSVExport',
description: '/docs/references/migrations/migration-csv-export.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.')
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('delimiter', ',', new Text(1), 'The character that separates each column value. Default is comma.', true)
->param('enclosure', '"', new Text(1), 'The character that encloses each column value. Default is double quotes.', true)
->param('escape', '"', new Text(1), 'The escape character for the enclosure character. Default is double quotes.', true)
->param('header', true, new Boolean(), 'Whether to include the header row with column names. Default is true.', true)
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
->inject('user')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $resourceId,
string $filename,
array $columns,
array $queries,
string $delimiter,
string $enclosure,
string $escape,
bool $header,
bool $notify,
Document $user,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Migration $queueForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
if (empty($databaseId)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
if (empty($collectionId)) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$validator = new Documents(
attributes: $collection->getAttribute('attributes', []),
indexes: $collection->getAttribute('indexes', []),
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
);
if (!$validator->isValid($parsedQueries)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => CSV::getName(),
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'bucketId' => 'default', // Always use internal bucket
'filename' => $filename,
'columns' => $columns,
'queries' => $queries,
'delimiter' => $delimiter,
'enclosure' => $enclosure,
'escape' => $escape,
'header' => $header,
'notify' => $notify,
'userInternalId' => $user->getSequence(),
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List migrations')
@ -460,9 +597,10 @@ App::get('/v1/migrations')
))
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->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);
} catch (QueryException $e) {
@ -501,7 +639,7 @@ App::get('/v1/migrations')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$migrations = $dbForProject->find('migrations', $queries);
$total = $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('migrations', $filterQueries, 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.");
}

View file

@ -18,6 +18,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -526,8 +527,8 @@ App::put('/v1/project/variables/:variableId')
))
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')

View file

@ -1,7 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
@ -46,6 +45,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;
@ -118,7 +118,7 @@ App::post('/v1/projects')
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
@ -678,9 +678,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) {
@ -1234,9 +1234,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')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@ -1251,7 +1252,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);
});
@ -1475,8 +1476,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) {
@ -1531,9 +1532,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);
@ -1548,7 +1550,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);
});
@ -1613,8 +1615,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) {
@ -1756,7 +1758,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)
@ -1813,9 +1836,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);
@ -1830,7 +1854,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);
});

View file

@ -2,7 +2,6 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@ -13,6 +12,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
@ -50,6 +50,7 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -77,7 +78,7 @@ App::post('/v1/storage/buckets')
))
->param('bucketId', '', new CustomId(), 'Unique 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('name', '', new Text(128), 'Bucket name')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@ -85,10 +86,11 @@ App::post('/v1/storage/buckets')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
@ -141,6 +143,7 @@ App::post('/v1/storage/buckets')
'compression' => $compression,
'encryption' => $encryption,
'antivirus' => $antivirus,
'transformations' => $transformations,
'search' => implode(' ', [$bucketId, $name]),
]));
@ -180,9 +183,10 @@ App::get('/v1/storage/buckets')
))
->param('queries', [], new Buckets(), '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(', ', Buckets::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);
@ -222,7 +226,7 @@ App::get('/v1/storage/buckets')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$buckets = $dbForProject->find('buckets', $queries);
$total = $dbForProject->count('buckets', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('buckets', $filterQueries, 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) {
@ -289,7 +293,7 @@ App::put('/v1/storage/buckets/:bucketId')
))
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('name', null, new Text(128), 'Bucket name', false)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@ -297,10 +301,11 @@ App::put('/v1/storage/buckets/:bucketId')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@ -314,6 +319,7 @@ App::put('/v1/storage/buckets/:bucketId')
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@ -327,7 +333,8 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('enabled', $enabled)
->setAttribute('encryption', $encryption)
->setAttribute('compression', $compression)
->setAttribute('antivirus', $antivirus));
->setAttribute('antivirus', $antivirus)
->setAttribute('transformations', $transformations));
$dbForProject->updateCollection('bucket_' . $bucket->getSequence(), $permissions, $fileSecurity);
@ -417,7 +424,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new CustomId(), 'File 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('file', [], new File(), 'Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https://appwrite.io/docs/products/storage/upload-download#input-file).', skipValidation: true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@ -430,8 +437,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -463,7 +470,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
if (!User::isApp($roles) && !User::isPrivileged($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@ -674,7 +681,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
$file = $file
->setAttribute('$permissions', $permissions)
@ -724,6 +737,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@ -785,14 +800,15 @@ App::get('/v1/storage/buckets/:bucketId/files')
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('queries', [], new Files(), '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(', ', Files::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('mode')
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -846,10 +862,10 @@ App::get('/v1/storage/buckets/:bucketId/files')
try {
if ($fileSecurity && !$valid) {
$files = $dbForProject->find('bucket_' . $bucket->getSequence(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT) : 0;
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getSequence(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT));
$total = $includeTotal ? Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT)) : 0;
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -892,8 +908,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -974,13 +990,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
@ -1115,7 +1135,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@ -1166,8 +1186,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -1327,8 +1347,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -1475,12 +1495,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Database $dbForPlatform, Document $project, string $mode, Device $deviceForFiles) {
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
@ -1497,15 +1516,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isInternal = $decoded['internal'] ?? false;
$disposition = $decoded['disposition'] ?? 'inline';
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@ -1551,7 +1574,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->setContentType($contentType)
->addHeader('Content-Security-Policy', 'script-src none;')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Content-Disposition', $disposition . '; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('X-Peak', \memory_get_peak_usage());
@ -1643,8 +1666,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
))
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('name', null, new Text(255), 'Name of the file', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('name', null, new Nullable(new Text(255)), 'Name of the file', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
@ -1654,8 +1677,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@ -1684,7 +1707,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@ -1768,8 +1791,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);

View file

@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@ -10,7 +9,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
@ -18,6 +17,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
@ -28,6 +28,9 @@ use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -48,10 +51,12 @@ 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\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
@ -85,8 +90,8 @@ App::post('/v1/teams')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
@ -169,9 +174,11 @@ App::get('/v1/teams')
))
->param('queries', [], new Teams(), '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(', ', Teams::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);
@ -211,7 +218,7 @@ App::get('/v1/teams')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('teams', $queries);
$total = $dbForProject->count('teams', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('teams', $filterQueries, 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.");
}
@ -466,15 +473,14 @@ App::post('/v1/teams/:teamId/memberships')
))
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.', true)
->param('email', '', new EmailValidator(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@ -493,9 +499,11 @@ App::post('/v1/teams/:teamId/memberships')
->inject('timelimit')
->inject('queueForStatsUsage')
->inject('plan')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) {
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$url = htmlentities($url);
if (empty($url)) {
@ -566,37 +574,53 @@ App::post('/v1/teams/:teamId/memberships')
try {
$userId = ID::unique();
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => empty($email) ? null : $email,
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
])));
$hash = $proofForPassword->hash($proofForPassword->generate());
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$userId = ID::unique();
$userDocument = new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => empty($email) ? null : $email,
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => $hash,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
try {
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -613,7 +637,7 @@ App::post('/v1/teams/:teamId/memberships')
Query::equal('teamInternalId', [$team->getSequence()]),
]);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
if ($membership->isEmpty()) {
$membershipId = ID::unique();
$membership = new Document([
@ -633,7 +657,7 @@ App::post('/v1/teams/:teamId/memberships')
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'secret' => $proofForToken->hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
@ -644,9 +668,8 @@ App::post('/v1/teams/:teamId/memberships')
if ($isPrivilegedUser || $isAppUser) {
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', Auth::hash($secret));
$membership->setAttribute('secret', $proofForToken->hash($secret));
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
@ -749,7 +772,6 @@ App::post('/v1/teams/:teamId/memberships')
->setName($invitee->getAttribute('name', ''))
->setVariables($emailVariables)
->trigger();
} elseif (!empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@ -838,10 +860,11 @@ App::get('/v1/teams/:teamId/memberships')
->param('teamId', '', new UID(), 'Team 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('project')
->inject('dbForProject')
->action(function (string $teamId, array $queries, string $search, Response $response, Document $project, Database $dbForProject) {
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -893,11 +916,11 @@ App::get('/v1/teams/:teamId/memberships')
collection: 'memberships',
queries: $queries,
);
$total = $dbForProject->count(
$total = $includeTotal ? $dbForProject->count(
collection: 'memberships',
queries: $filterQueries,
max: 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.");
}
@ -912,8 +935,8 @@ App::get('/v1/teams/:teamId/memberships')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@ -1003,8 +1026,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@ -1069,8 +1092,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@ -1099,8 +1122,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
@ -1185,7 +1208,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) {
->inject('store')
->inject('proofForToken')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@ -1204,7 +1229,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
throw new Exception(Exception::TEAM_INVALID_SECRET);
}
@ -1238,9 +1263,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
@ -1250,9 +1275,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
],
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
@ -1264,14 +1289,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::setRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey() . '_legacy',
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@ -1279,8 +1309,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey(),
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@ -1430,11 +1460,12 @@ App::get('/v1/teams/:teamId/logs')
))
->param('teamId', '', new UID(), 'Team 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 $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -1503,7 +1534,7 @@ App::get('/v1/teams/:teamId/logs')
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});

View file

@ -1,7 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Password;
@ -16,7 +15,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;
@ -32,6 +31,18 @@ use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Hash;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Bcrypt;
use Utopia\Auth\Hashes\MD5;
use Utopia\Auth\Hashes\PHPass;
use Utopia\Auth\Hashes\Plaintext;
use Utopia\Auth\Hashes\Scrypt;
use Utopia\Auth\Hashes\ScryptModified;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Password as ProofsPassword;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -49,21 +60,22 @@ 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;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$plaintextPassword = $password;
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
if (!empty($email)) {
@ -97,7 +109,29 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$hashedPassword = null;
$isHashed = !$hash instanceof Plaintext;
$defaultHash = new ProofsPassword();
if (!empty($password)) {
if (!$isHashed) { // Password was never hashed, hash it with the default hash
$hashedPassword = $defaultHash->hash($password);
$hash = $defaultHash->getHash();
} else {
$hashedPassword = $password;
}
} else {
// when password is not provided, plaintext was set as the default hash causing the issue
$hash = $defaultHash->getHash();
$isHashed = !$hash instanceof Plaintext;
}
$user = new Document([
'$id' => $userId,
'$permissions' => [
@ -111,11 +145,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'phoneVerification' => false,
'status' => true,
'labels' => [],
'password' => $password,
'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
'password' => $hashedPassword,
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
'hash' => $hash->getName(),
'hashOptions' => $hash->getOptions(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@ -124,9 +158,14 @@ 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') {
if (!$isHashed && !empty($password)) {
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
}
@ -208,8 +247,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')
@ -217,7 +256,9 @@ App::post('/v1/users')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@ -243,7 +284,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')
@ -251,7 +292,10 @@ App::post('/v1/users/bcrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -278,7 +322,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')
@ -286,7 +330,9 @@ App::post('/v1/users/md5')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -313,7 +359,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')
@ -321,7 +367,9 @@ App::post('/v1/users/argon2')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -348,7 +396,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)
@ -357,13 +405,12 @@ App::post('/v1/users/sha')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = '{}';
$sha = new Sha();
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
$sha->setVersion($passwordVersion);
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -390,7 +437,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')
@ -398,7 +445,9 @@ App::post('/v1/users/phpass')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -425,7 +474,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.')
@ -438,15 +487,15 @@ App::post('/v1/users/scrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
->setCpuCost($passwordCpu)
->setMemoryCost($passwordMemory)
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -473,7 +522,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.')
@ -484,7 +533,13 @@ App::post('/v1/users/scrypt-modified')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -527,7 +582,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,9 +660,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')
->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);
@ -647,10 +703,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) {
@ -784,32 +840,29 @@ 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);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {
/** @var Document $session */
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', false);
$sessions[$key] = $session;
}
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
'total' => $includeTotal ? count($sessions) : 0,
]), Response::MODEL_SESSION_LIST);
});
@ -833,43 +886,38 @@ 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);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Set internal queries
$queries[] = Query::equal('userInternalId', [$user->getSequence()]);
$memberships = array_map(function ($membership) use ($dbForProject, $user) {
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'));
return $membership;
}, $dbForProject->find('memberships', $queries));
$response->dynamic(new Document([
'memberships' => $memberships,
'total' => count($memberships),
'total' => $includeTotal ? count($memberships) : 0,
]), Response::MODEL_MEMBERSHIP_LIST);
});
@ -892,46 +940,38 @@ 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);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => ID::custom($log['data']['userId']),
@ -952,9 +992,7 @@ App::get('/v1/users/:userId/logs')
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
@ -965,7 +1003,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);
});
@ -989,23 +1027,21 @@ 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()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('userId', [$userId]);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@ -1013,25 +1049,21 @@ App::get('/v1/users/:userId/targets')
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$targetId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
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.");
}
@ -1060,9 +1092,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);
@ -1073,7 +1106,6 @@ App::get('/v1/users/identities')
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@ -1083,26 +1115,21 @@ App::get('/v1/users/identities')
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$identityId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
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.");
}
@ -1336,12 +1363,17 @@ App::patch('/v1/users/:userId/password')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
// Create Argon2 hasher with default settings
$hasher = new Argon2();
$newPassword = $hasher->hash($password);
$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = $user->getAttribute('passwordHistory', []);
if ($historyLimit > 0) {
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$validator = new PasswordHistory($history, $hash);
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
}
@ -1354,8 +1386,8 @@ App::patch('/v1/users/:userId/password')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -1397,7 +1429,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')
@ -1432,9 +1464,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 {
@ -1695,7 +1738,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);
}
@ -2168,17 +2211,19 @@ App::post('/v1/users/:userId/sessions')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
->inject('store')
->inject('proofForToken')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = $proofForToken->generate();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
@ -2186,8 +2231,8 @@ App::post('/v1/users/:userId/sessions')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'provider' => SESSION_PROVIDER_SERVER,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
@ -2211,8 +2256,13 @@ App::post('/v1/users/:userId/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
$session
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('secret', $encoded)
->setAttribute('countryName', $countryName);
$queueForEvents
@ -2247,7 +2297,7 @@ App::post('/v1/users/:userId/tokens')
))
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@ -2259,15 +2309,17 @@ App::post('/v1/users/:userId/tokens')
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator($length);
$proofForToken = new Token($length);
$proofForToken->setHash(new Sha());
$secret = $proofForToken->generate();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'type' => TOKEN_TYPE_GENERIC,
'secret' => $proofForToken->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
@ -2580,7 +2632,8 @@ App::post('/v1/users/:userId/jwts')
$session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document();
} else {
// Find by ID
foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */
foreach ($sessions as $loopSession) {
/** @var Utopia\Database\Document $loopSession */
if ($loopSession->getId() == $sessionId) {
$session = $loopSession;
break;

View file

@ -14,9 +14,12 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Swoole\Coroutine\WaitGroup;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Adapters\Dotenv as ConfigDotenv;
use Utopia\Config\Config;
use Utopia\Config\Exceptions\Parse;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@ -28,13 +31,24 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
use Utopia\Detector\Detection\Framework\Flutter;
use Utopia\Detector\Detection\Framework\Lynx;
use Utopia\Detector\Detection\Framework\NextJs;
use Utopia\Detector\Detection\Framework\Nuxt;
use Utopia\Detector\Detection\Framework\React;
use Utopia\Detector\Detection\Framework\ReactNative;
use Utopia\Detector\Detection\Framework\Remix;
use Utopia\Detector\Detection\Framework\Svelte;
use Utopia\Detector\Detection\Framework\SvelteKit;
use Utopia\Detector\Detection\Framework\TanStackStart;
use Utopia\Detector\Detection\Framework\Vue;
use Utopia\Detector\Detection\Packager\NPM;
use Utopia\Detector\Detection\Packager\PNPM;
use Utopia\Detector\Detection\Packager\Yarn;
@ -58,6 +72,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
@ -166,7 +181,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
@ -237,7 +252,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@ -458,7 +473,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@ -818,7 +833,10 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
$files = \array_column($files, 'name');
$languages = $github->listRepositoryLanguages($owner, $repositoryName);
$detector = new Packager($files);
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
@ -828,6 +846,14 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$output = new Document([
'framework' => '',
'installCommand' => '',
@ -835,14 +861,27 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
'outputDirectory' => '',
]);
$detector = new Framework($files, $packager);
$detector = new Framework($packager);
$detector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$detector->addInput($file, Framework::INPUT_FILE);
}
$detector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new SvelteKit())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Remix());
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$framework = $detector->detect();
@ -877,7 +916,18 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector = new Runtime($strategy, $packager);
if ($strategy === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
@ -919,6 +969,46 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse $err) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$variables = [];
foreach ($envs as $key => $value) {
$variables[] = [
'name' => $key,
'value' => $value,
];
}
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
});
@ -946,10 +1036,11 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->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)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) {
->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
@ -965,11 +1056,20 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$page = 1;
$perPage = 4;
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET));
$limit = !empty($limitQuery) ? $limitQuery->getValue() : 4;
$offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0;
if ($offset % $limit !== 0) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit');
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
$repos = $github->searchRepositories($owner, $page, $perPage, $search);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
@ -984,7 +1084,10 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$files = $github->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$detector = new Packager($files);
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
@ -994,14 +1097,35 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$frameworkDetector = new Framework($files, $packager);
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$frameworkDetector = new Framework($packager);
$frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$frameworkDetector->addInput($file, Framework::INPUT_FILE);
}
$frameworkDetector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new SvelteKit())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Remix());
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$detectedFramework = $frameworkDetector->detect();
@ -1026,7 +1150,16 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector = new Runtime($strategy, $packager);
if ($strategy === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
@ -1060,6 +1193,44 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$repo['runtime'] = $runtimeWithVersion ?? '';
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $repo, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$repo['variables'] = [];
foreach ($envs as $key => $value) {
$repo['variables'][] = [
'name' => $key,
'value' => $value,
];
}
return $repo;
};
}, $repos));
@ -1070,7 +1241,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$response->dynamic(new Document([
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => \count($repos),
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
});
@ -1374,7 +1545,7 @@ App::post('/v1/vcs/github/events')
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$dbForPlatform->deleteDocument('installations', $installation->getId());
Authorization::skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@ -1458,11 +1629,12 @@ App::get('/v1/vcs/installations')
))
->param('queries', [], new Installations(), '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(', ', Installations::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('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@ -1503,7 +1675,7 @@ App::get('/v1/vcs/installations')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForPlatform->find('installations', $queries);
$total = $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForPlatform->count('installations', $filterQueries, 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.");
}
@ -1624,7 +1796,8 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));

View file

@ -4,7 +4,6 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
@ -17,12 +16,14 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Transformation\Adapter\Preview;
use Appwrite\Transformation\Transformation;
use Appwrite\Utopia\Database\Documents\User as DBUser;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@ -222,7 +223,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
*/
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
// Security checks to mark authorized true
@ -906,6 +907,9 @@ App::init()
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
}
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
}
$domain = $request->getHostname();
@ -1253,10 +1257,10 @@ App::error()
}
/**
* If its not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!DBUser::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@ -1355,6 +1359,7 @@ App::error()
case 409: // Error allowed publicly
case 412: // Error allowed publicly
case 416: // Error allowed publicly
case 422: // Error allowed publicly
case 429: // Error allowed publicly
case 451: // Error allowed publicly
case 501: // Error allowed publicly
@ -1612,7 +1617,7 @@ App::get('/_appwrite/authorize')
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addCookie(COOKIE_NAME_PREVIEW, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $host . $path);

View file

@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Audit;
@ -16,13 +15,13 @@ use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -34,7 +33,7 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
@ -54,7 +53,20 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
};
if (array_key_exists($replace, $params)) {
$label = \str_replace($find, $params[$replace], $label);
$replacement = $params[$replace];
// Convert to string if it's not already a string
if (!is_string($replacement)) {
if (is_array($replacement)) {
$replacement = json_encode($replacement);
} elseif (is_object($replacement) && method_exists($replacement, '__toString')) {
$replacement = (string)$replacement;
} elseif (is_scalar($replacement)) {
$replacement = (string)$replacement;
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
}
$label = \str_replace($find, $replacement, $label);
}
}
return $label;
@ -219,42 +231,97 @@ App::init()
->inject('mode')
->inject('team')
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
$route = $utopia->getRoute();
/**
* Handle user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
*
* Project & Role Validation:
* 1. Check if the project is empty. If so, throw an exception.
* 2. Get the roles configuration.
* 3. Determine the role for the user based on the user document.
* 4. Get the scopes for the role.
*
* API Key Authentication:
* 5. If there is an API key:
* - Verify no user session exists simultaneously
* - Check if key is expired
* - Set role and scopes from API key
* - Handle special app role case
* - For standard keys, update last accessed time
*
* User Activity:
* 6. If the project is not the console and user is not admin:
* - Update user's last activity timestamp
*
* Access Control:
* 7. Get the method from the route
* 8. Validate namespace permissions
* 9. Validate scope permissions
* 10. Check if user is blocked
*
* Security Checks:
* 11. Verify password status (check if reset required)
* 12. Validate MFA requirements:
* - Check if MFA is enabled
* - Verify email status
* - Verify phone status
* - Verify authenticator status
* 13. Handle Multi-Factor Authentication:
* - Check remaining required factors
* - Validate factor completion
* - Throw exception if factors incomplete
*/
// Step 1: Check if project is empty
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
// Step 2: Get roles configuration
$roles = Config::getParam('roles', []);
// Step 3: Determine role for user
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
$role = $user->isEmpty()
? Role::guests()->toString()
: Role::users()->toString();
// Step 4: Get scopes for the role
$scopes = $roles[$role]['scopes'];
// API Key authentication
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
// Set role and scopes from API key
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@ -263,6 +330,7 @@ App::init()
$queueForAudits->setUser($user);
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
@ -328,11 +396,11 @@ App::init()
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
foreach ($user->getRoles() as $authRole) {
Authorization::setRole($authRole);
}
// Update project last activity
// Step 6: Update project and user last activity
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
@ -341,7 +409,6 @@ App::init()
}
}
// Update user last activity
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
@ -355,6 +422,7 @@ App::init()
}
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
@ -372,27 +440,29 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Do now allow access if scope is not allowed
// Step 9: Validate scope permissions
$allowed = (array)$route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
}
// Do not allow access to blocked accounts
// Step 10: Check if user is blocked
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
// Step 11: Verify password status
if ($user->getAttribute('reset')) {
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
}
// Step 12: Validate MFA requirements
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
@ -400,6 +470,7 @@ App::init()
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
// Step 13: Handle Multi-Factor Authentication
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
@ -439,7 +510,7 @@ App::init()
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@ -470,8 +541,8 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
@ -526,7 +597,7 @@ App::init()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
@ -569,7 +640,7 @@ App::init()
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
$key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@ -580,6 +651,10 @@ App::init()
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
$usageMetric = $route->getLabel('usage.metric', null);
if ($usageMetric === METRIC_AVATARS_SCREENSHOTS_GENERATED) {
$queueForStatsUsage->disableMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED);
}
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
@ -588,12 +663,16 @@ App::init()
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
@ -618,7 +697,7 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@ -718,7 +797,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@ -766,7 +845,7 @@ App::shutdown()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
@ -777,10 +856,10 @@ App::shutdown()
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'type' => ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@ -870,7 +949,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {

View file

@ -1,7 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Utopia\App;
@ -20,7 +20,7 @@ App::init()
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
@ -49,8 +49,8 @@ App::init()
$route = $utopia->match($request);
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;

View file

@ -1,42 +1,45 @@
<?php
use Utopia\Config\Adapters\PHP;
use Utopia\Config\Config;
require_once __DIR__ . '/../config/storage/resource_limits.php';
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php');
Config::load('events', __DIR__ . '/../config/events.php');
Config::load('auth', __DIR__ . '/../config/auth.php');
Config::load('apis', __DIR__ . '/../config/apis.php'); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php');
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/../config/platforms.php');
Config::load('console', __DIR__ . '/../config/console.php');
Config::load('collections', __DIR__ . '/../config/collections.php');
Config::load('frameworks', __DIR__ . '/../config/frameworks.php');
Config::load('runtimes', __DIR__ . '/../config/runtimes.php');
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php');
Config::load('usage', __DIR__ . '/../config/usage.php');
Config::load('roles', __DIR__ . '/../config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php'); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php'); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php'); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php');
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php');
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php');
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php');
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php');
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php');
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php');
Config::load('specifications', __DIR__ . '/../config/specifications.php');
Config::load('templates-function', __DIR__ . '/../config/templates/function.php');
Config::load('templates-site', __DIR__ . '/../config/templates/site.php');
$configAdapter = new PHP();
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('platforms', __DIR__ . '/../config/platforms.php', $configAdapter);
Config::load('console', __DIR__ . '/../config/console.php', $configAdapter);
Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapter);
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);
Config::load('runtimes', __DIR__ . '/../config/runtimes.php', $configAdapter);
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapter);
Config::load('usage', __DIR__ . '/../config/usage.php', $configAdapter);
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php', $configAdapter);
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php', $configAdapter);
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php', $configAdapter);
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php', $configAdapter);
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php', $configAdapter);
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php', $configAdapter);
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php', $configAdapter);
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php', $configAdapter);
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php', $configAdapter);
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php', $configAdapter);
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php', $configAdapter);
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php', $configAdapter);
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php', $configAdapter);
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php', $configAdapter);
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php', $configAdapter);
Config::load('specifications', __DIR__ . '/../config/specifications.php', $configAdapter);
Config::load('templates-function', __DIR__ . '/../config/templates/function.php', $configAdapter);
Config::load('templates-site', __DIR__ . '/../config/templates/site.php', $configAdapter);

View file

@ -31,6 +31,7 @@ const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
const APP_LIMIT_DATABASE_BATCH = 100; // Default maximum batch size for database operations
const APP_LIMIT_DATABASE_TRANSACTION = 100; // Default maximum operations per transaction
const APP_KEY_ACCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
@ -55,6 +56,10 @@ const APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER = 300 * 1000; // 5 minutes
const APP_DATABASE_TIMEOUT_MILLISECONDS_TASK = 300 * 1000; // 5 minutes
const APP_DATABASE_QUERY_MAX_VALUES = 500;
const APP_DATABASE_ENCRYPT_SIZE_MIN = 150;
const APP_DATABASE_TXN_TTL_MIN = 60; // 1 minute
const APP_DATABASE_TXN_TTL_MAX = 3600; // 1 hour
const APP_DATABASE_TXN_TTL_DEFAULT = 300; // 5 minutes
const APP_DATABASE_TXN_MAX_OPERATIONS = 100; // Maximum operations per transaction
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_SITES = '/storage/sites';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
@ -85,6 +90,71 @@ const APP_PLATFORM_CLIENT = 'client';
const APP_PLATFORM_CONSOLE = 'console';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
/**
* JWT for Resource Tokens.
*/
const RESOURCE_TOKEN_ALGORITHM = 'HS256';
const RESOURCE_TOKEN_MAX_AGE = 86400 * 365 * 10; /* 10 years */
const RESOURCE_TOKEN_LEEWAY = 10; // 10 seconds
/**
* Token Expiration times.
*/
const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
const TOKEN_LENGTH_MAGIC_URL = 64;
const TOKEN_LENGTH_VERIFICATION = 256;
const TOKEN_LENGTH_RECOVERY = 256;
const TOKEN_LENGTH_OAUTH2 = 64;
const TOKEN_LENGTH_SESSION = 256;
/**
* Token Types.
*/
const TOKEN_TYPE_LOGIN = 1; // Deprecated
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
const TOKEN_TYPE_MAGIC_URL = 5;
const TOKEN_TYPE_PHONE = 6;
const TOKEN_TYPE_OAUTH2 = 7;
const TOKEN_TYPE_GENERIC = 8;
const TOKEN_TYPE_EMAIL = 9; // OTP
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
const SESSION_PROVIDER_PHONE = 'phone';
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_GUEST = 'guest';
/**
* MFA
*/
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
@ -104,8 +174,11 @@ const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
const DELETE_TYPE_EXPIRED_TRANSACTIONS = 'expired_transactions';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_SITES = 'sites';
const DELETE_TYPE_FUNCTIONS = 'functions';
@ -128,6 +201,7 @@ const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Message types
@ -260,6 +334,9 @@ const METRIC_SITES_OUTBOUND = 'sites.outbound';
const METRIC_SITES_ID_REQUESTS = 'sites.{siteInternalId}.requests';
const METRIC_SITES_ID_INBOUND = 'sites.{siteInternalId}.inbound';
const METRIC_SITES_ID_OUTBOUND = 'sites.{siteInternalId}.outbound';
const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
@ -283,3 +360,6 @@ const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';
const SCHEDULE_RESOURCE_TYPE_EXECUTION = 'execution';
const SCHEDULE_RESOURCE_TYPE_FUNCTION = 'function';
const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message';
/** Preview cookie */
const COOKIE_NAME_PREVIEW = 'a_jwt_console';

View file

@ -39,6 +39,7 @@ if (!App::isProduction()) {
PublicDomain::allow(['request-catcher-sms']);
PublicDomain::allow(['request-catcher-webhook']);
}
$register->set('logger', function () {
// Register error logger
$providerName = System::getEnv('_APP_LOGGING_PROVIDER', '');
@ -97,6 +98,51 @@ $register->set('logger', function () {
return new Logger($adapter);
});
$register->set('realtimeLogger', function () {
// Register error logger for realtime, falls back to default logging config
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG_REALTIME', '')
?: System::getEnv('_APP_LOGGING_CONFIG', '');
if (empty($providerConfig)) {
return;
}
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()],
'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()],
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
return;
}
if (!Logger::hasProvider($providerName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled");
}
try {
$adapter = match ($providerName) {
'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']),
'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']),
'raygun' => new Raygun($providerConfig['key']),
'appsignal' => new AppSignal($providerConfig['key']),
default => null
};
} catch (Throwable $th) {
$adapter = null;
}
if ($adapter === null) {
Console::error("Logging provider not supported. Logging is disabled");
return;
}
return new Logger($adapter);
});
$register->set('pools', function () {
$group = new Group();
@ -326,7 +372,7 @@ $register->set('smtp', function () {
return $mail;
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2024-09.mmdb');
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
});
$register->set('passwordsDictionary', function () {
$content = \file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords');

View file

@ -2,8 +2,8 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@ -22,10 +22,18 @@ use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@ -151,7 +159,7 @@ App::setResource('queueForMigrations', function (Publisher $publisher) {
App::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
App::setResource('platforms', function (Request $request, Document $console, Document $project) {
App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
@ -190,82 +198,126 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
], Document::SET_TYPE_APPEND);
}
$origin = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($origin)) {
$origin = \parse_url($request->getReferer(), PHP_URL_HOST);
}
// Safe if rule with same project ID exists
if (!empty($origin)) {
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_WEB,
'name' => $origin,
'hostname' => $origin,
], Document::SET_TYPE_APPEND);
}
}
return [
...$console->getAttribute('platforms', []),
...$project->getAttribute('platforms', []),
];
}, ['request', 'console', 'project']);
}, ['request', 'console', 'project', 'dbForPlatform']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
/**
* Handles user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
* 1. Checks the cookie based on mode:
* - If in admin mode, uses console project id for key.
* - Otherwise, sets the key using the project ID
* 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`.
* - If this method is used, returns the header: `X-Debug-Fallback: true`.
* 3. Fetches the user document from the appropriate database based on the mode.
* 4. If the user document is empty or the session key cannot be verified, sets an empty user document.
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
* overwriting the previous value.
*/
Authorization::setDefaultStatus(true);
Auth::setCookieName('a_session_' . $project->getId());
$store->setKey('a_session_' . $project->getId());
if (APP_MODE_ADMIN === $mode) {
Auth::setCookieName('a_session_' . $console->getId());
$store->setKey('a_session_' . $console->getId());
}
$session = Auth::decodeSession(
$store->decode(
$request->getCookie(
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
$store->getKey(), // Get sessions
$request->getCookie($store->getKey() . '_legacy', '')
)
);
// Get session from header for SSR clients
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$session = Auth::decodeSession($sessionHeader);
$store->decode($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) {
if ($response) { // if in http context - add debug header
$response->addHeader('X-Debug-Fallback', 'false');
}
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
}
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$user = new Document([]);
if (!empty(Auth::$unique)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} elseif (!$project->isEmpty()) {
if ($project->getId() === 'console') {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
$user = null;
if (APP_MODE_ADMIN === $mode) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} else {
if (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
}
if (
!$user ||
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) { // Validate user has valid login token
$user = new Document([]);
$user = new User([]);
}
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
@ -273,18 +325,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
// $user = new Document([]);
// }
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@ -293,20 +341,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId)) {
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new Document([]);
$user = new User([]);
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
@ -324,31 +370,61 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $project;
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
$sessions = $user->getAttribute('sessions', []);
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
if (!$sessionId) {
return;
}
foreach ($sessions as $session) {/** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
if ($sessionId === $session->getId()) {
return $session;
}
}
return;
}, ['user']);
}, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store {
return new Store();
});
App::setResource('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
->setTimeCost(5)
->setThreads(1);
$password = new Password();
$password
->setHash($hash);
return $password;
});
App::setResource('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
App::setResource('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
@ -369,13 +445,14 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setTenant((int) $project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@ -398,6 +475,8 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
return $database;
}, ['pools', 'cache']);
@ -422,13 +501,14 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setTenant((int) $project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@ -458,7 +538,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return function (?Document $project = null) use ($pools, $cache, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
$database->setTenant((int) $project->getSequence());
return $database;
}
@ -473,7 +553,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
$database->setTenant((int) $project->getSequence());
}
return $database;
@ -501,7 +581,7 @@ App::setResource('redis', function () {
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
@$redis->pconnect($host, (int) $port);
if ($pass) {
$redis->auth($pass);
}
@ -525,7 +605,7 @@ App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) {
App::setResource('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
App::setResource('deviceForImports', function ($project, Telemetry $telemetry) {
App::setResource('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
App::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) {
@ -714,7 +794,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
// NOTE: `params` and `urls` are not used internally in the `Schema::build` function below!
$params = [
'list' => function (string $databaseId, string $collectionId, array $args) {
return [ 'queries' => $args['queries']];
return ['queries' => $args['queries']];
},
'create' => function (string $databaseId, string $collectionId, array $args) {
$id = $args['id'] ?? 'unique()';
@ -923,7 +1003,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
try {
$payload = $jwt->decode($tokenJWT);
@ -963,7 +1044,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
}
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), - APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
}
@ -1005,24 +1086,6 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef
return $referrer;
}
// Safe if rule with same project ID exists
if (!empty($origin)) {
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
return $referrer;
}
}
// Unsafe; Localhost is always safe for ease of local development
$origin = 'localhost';
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
@ -1030,3 +1093,7 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
App::setResource('transactionState', function (Database $dbForProject) {
return new TransactionState($dbForProject);
}, ['dbForProject']);

View file

@ -1,11 +1,11 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Http\Request as SwooleRequest;
@ -16,6 +16,9 @@ use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@ -67,6 +70,8 @@ if (!function_exists('getConsoleDB')) {
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
$database->setDocumentType('users', User::class);
return $database;
}
}
@ -118,6 +123,8 @@ if (!function_exists('getProjectDB')) {
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
$database->setDocumentType('users', User::class);
return $databases[$project->getSequence()] = $database;
}
}
@ -236,7 +243,7 @@ $adapter
$server = new Server($adapter);
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('logger');
$logger = $register->get('realtimeLogger');
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
@ -457,9 +464,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection);
@ -526,14 +534,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$timelimit = $app->getResource('timelimit');
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var Document $user */
$user = $app->getResource('user'); /** @var User $user */
/*
* Abuse Check
@ -563,7 +571,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@ -604,11 +612,18 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$code = 500;
}
$message = $th->getMessage();
// sanitize 0 && 5xx errors
if (($code === 0 || $code >= 500) && !App::isDevelopment()) {
$message = 'Error: Server Error';
}
$response = [
'type' => 'error',
'data' => [
'code' => $code,
'message' => $th->getMessage()
'message' => $message
]
];
@ -671,21 +686,31 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$session = Auth::decodeSession($message['data']['session']);
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$store = new Store();
$user = $database->getDocument('users', Auth::$unique);
$store->decode($message['data']['session']);
/** @var User $user */
$user = $database->getDocument('users', $store->getProperty('id', ''));
/**
* TODO:
* Moving forward, we should try to use our dependency injection container
* to inject the proof for token.
* This way we will have one source of truth for the proof for token.
*/
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
@ -705,11 +730,23 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
} catch (Throwable $th) {
$code = $th->getCode();
if (!is_int($code)) {
$code = 500;
}
$message = $th->getMessage();
// sanitize 0 && 5xx errors
if (($code === 0 || $code >= 500) && !App::isDevelopment()) {
$message = 'Error: Server Error';
}
$response = [
'type' => 'error',
'data' => [
'code' => $th->getCode(),
'message' => $th->getMessage()
'code' => $code,
'message' => $message
]
];

View file

@ -179,7 +179,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:7.0.2
image: <?php echo $organization; ?>/console:7.4.7
restart: unless-stopped
networks:
- appwrite
@ -849,7 +849,7 @@ $image = $this->getParam('image', '');
- _APP_DB_PASS
appwrite-assistant:
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
container_name: appwrite-assistant
<<: *x-logging
restart: unless-stopped
@ -857,9 +857,9 @@ $image = $this->getParam('image', '');
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-browser:
image: appwrite/browser:0.2.4
image: appwrite/browser:0.3.2
container_name: appwrite-browser
<<: *x-logging
restart: unless-stopped

View file

@ -17,6 +17,7 @@ use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@ -55,7 +56,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register)
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'register']);
@ -86,6 +87,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@ -349,7 +351,7 @@ Server::setResource('deviceForSites', function (Document $project, Telemetry $te
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForImports', function (Document $project, Telemetry $telemetry) {
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);

View file

@ -1,5 +1,4 @@
{
"name": "appwrite/server-ce",
"description": "End to end backend server for frontend and mobile apps.",
"type": "project",
@ -49,21 +48,23 @@
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "1.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "1.*",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.8.*",
"utopia-php/dns": "0.3.*",
"utopia-php/config": "1.*.*",
"utopia-php/database": "3.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.1.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.4.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.18.*",
"utopia-php/migration": "1.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.3.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
@ -74,7 +75,7 @@
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.11.*",
"utopia-php/vcs": "0.13.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",

1096
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -219,7 +219,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.0.2
image: appwrite/console:7.4.11
restart: unless-stopped
networks:
- appwrite
@ -285,6 +285,7 @@ services:
- _APP_DB_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-audits:
@ -698,6 +699,7 @@ services:
- appwrite
volumes:
- appwrite-imports:/storage/imports:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
@ -949,7 +951,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
networks:
- appwrite
environment:
@ -957,7 +959,7 @@ services:
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.2.4
image: appwrite/browser:0.3.2
networks:
- appwrite
@ -966,7 +968,7 @@ services:
hostname: exc1
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.11.0
image: openruntimes/executor:0.11.4
restart: unless-stopped
networks:
- appwrite

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0

View file

@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0

View file

@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0

View file

@ -0,0 +1,22 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Account account = new Account(client);
account.createEmailVerification(
"https://example.com", // url
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -13,7 +13,7 @@ account.createOAuth2Session(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

View file

@ -13,7 +13,7 @@ account.createOAuth2Token(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

View file

@ -9,7 +9,8 @@ Client client = new Client(context)
Account account = new Account(client);
account.listIdentities(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

View file

@ -9,7 +9,8 @@ Client client = new Client(context)
Account account = new Account(client);
account.listLogs(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

View file

@ -0,0 +1,23 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Account account = new Account(client);
account.updateEmailVerification(
"<USER_ID>", // userId
"<SECRET>", // secret
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -9,10 +9,10 @@ Client client = new Client(context)
Account account = new Account(client);
account.updatePrefs(
mapOf(
"language" to "en",
"timezone" to "UTC",
"darkTheme" to true
Map.of(
"language", "en",
"timezone", "UTC",
"darkTheme", true
), // prefs
new CoroutineCallback<>((result, error) -> {
if (error != null) {

View file

@ -0,0 +1,47 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Avatars;
import io.appwrite.enums.Theme;
import io.appwrite.enums.Timezone;
import io.appwrite.enums.Output;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Avatars avatars = new Avatars(client);
avatars.getScreenshot(
"https://example.com", // url
Map.of(
"Authorization", "Bearer token123",
"X-Custom-Header", "value"
), // headers (optional)
1920, // viewportWidth (optional)
1080, // viewportHeight (optional)
2, // scale (optional)
Theme.LIGHT, // theme (optional)
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", // userAgent (optional)
true, // fullpage (optional)
"en-US", // locale (optional)
Timezone.AFRICA_ABIDJAN, // timezone (optional)
37.7749, // latitude (optional)
-122.4194, // longitude (optional)
100, // accuracy (optional)
true, // touch (optional)
List.of("geolocation", "notifications"), // permissions (optional)
3, // sleep (optional)
800, // width (optional)
600, // height (optional)
85, // quality (optional)
Output.JPG, // output (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -1,5 +1,7 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
@ -12,14 +14,15 @@ databases.createDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf(
"username" to "walter.obrien",
"email" to "walter.obrien@example.com",
"fullName" to "Walter O'Brien",
"age" to 30,
"isAdmin" to false
Map.of(
"username", "walter.obrien",
"email", "walter.obrien@example.com",
"fullName", "Walter O'Brien",
"age", 30,
"isAdmin", false
), // data
listOf("read("any")"), // permissions (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

View file

@ -0,0 +1,31 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Databases databases = new Databases(client);
databases.createOperations(
"<TRANSACTION_ID>", // transactionId
List.of(Map.of(
"action", "create",
"databaseId", "<DATABASE_ID>",
"collectionId", "<COLLECTION_ID>",
"documentId", "<DOCUMENT_ID>",
"data", Map.of(
"name", "Walter O'Brien"
)
)), // operations (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,22 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Databases databases = new Databases(client);
databases.createTransaction(
60, // ttl (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -15,6 +15,7 @@ databases.decrementDocumentAttribute(
"", // attribute
0, // value (optional)
0, // min (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

Some files were not shown because too many files have changed in this diff Show more