Merge branch '1.8.x' into allow-custom-sender
4
.env
|
|
@ -22,7 +22,7 @@ _APP_OPTIONS_FORCE_HTTPS=disabled
|
|||
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
_APP_DNS=8.8.8.8
|
||||
_APP_DOMAIN=traefik
|
||||
_APP_DOMAIN=appwrite.test
|
||||
_APP_CONSOLE_DOMAIN=localhost
|
||||
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
||||
_APP_DOMAIN_SITES=sites.localhost
|
||||
|
|
@ -124,4 +124,4 @@ _APP_MESSAGE_PUSH_TEST_DSN=
|
|||
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
_APP_PROJECT_REGIONS=default
|
||||
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
|
||||
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
|
||||
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
|
||||
|
|
|
|||
616
.github/workflows/issue-triage.lock.yml
generated
vendored
76
.github/workflows/issue-triage.md
vendored
|
|
@ -8,41 +8,68 @@ on:
|
|||
|
||||
permissions: read-all
|
||||
|
||||
# Add stricter error-detection patterns so issue text doesn't trigger agent error detection.
|
||||
# After merging this file run `gh aw compile` to regenerate the lock file.
|
||||
env:
|
||||
GH_AW_ERROR_PATTERNS: >-
|
||||
[
|
||||
{"id":"gh-action-error","pattern":"^::(error)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - error"},
|
||||
{"id":"gh-action-warning","pattern":"^::(warning)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - warning"},
|
||||
{"id":"bracketed-level","pattern":"^\\[(ERROR|CRITICAL|WARNING|WARN)\\]\\s+(.+)","level_group":1,"message_group":2,"description":"Bracketed log level at start of line"},
|
||||
{"id":"timestamped-copilot","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z\\s+\\[(ERROR|WARN|WARNING|CRITICAL)\\]\\s+(.+)","level_group":1,"message_group":3,"description":"Timestamped Copilot CLI messages"}
|
||||
]
|
||||
|
||||
network: defaults
|
||||
|
||||
safe-outputs:
|
||||
add-labels:
|
||||
max: 5
|
||||
max: 100
|
||||
target: "*"
|
||||
add-comment:
|
||||
max: 10
|
||||
target: "*"
|
||||
|
||||
tools:
|
||||
web-fetch:
|
||||
web-search:
|
||||
github:
|
||||
toolsets:
|
||||
- default
|
||||
- labels
|
||||
|
||||
timeout_minutes: 10
|
||||
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.
|
||||
You're a triage assistant for GitHub issues. Your task is to analyze issues that were either created in the last 24 hours or updated (with a new comment) 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).
|
||||
1. First, use the `list_issues` tool to retrieve all issues created or updated in the last 24 hours. The `since` parameter filters by the issue's `updated_at` timestamp, which includes both newly created issues and recently commented issues. Calculate the timestamp from 24 hours ago (example: 2025-11-06T20:27:14Z for reference) and use it for the `since` parameter.
|
||||
|
||||
2. For each issue found, perform the following triage tasks:
|
||||
|
||||
3. Select appropriate labels for the issue from the provided list.
|
||||
3. Use the `get_comments` tool to retrieve all the comments on the issue.
|
||||
|
||||
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.
|
||||
4. Check for spam and quality issue descriptions and comments first:
|
||||
|
||||
- **Non-English Content**: If the issue is primarily written in a non-English language, add a respectful and appreciative comment explaining that while you appreciate their contribution, the majority of the community communicates in English and kindly ask them to repost in English so everyone can follow along and help. Provide a friendly translation of your message in their language if possible.
|
||||
- **Multiple Topics**: If the issue discusses multiple unrelated topics or problems, add a comment explaining that each issue should focus on one clear topic so the team can effectively solve the right problem. Politely ask them to split it into separate issues.
|
||||
- **Obvious Spam or Bot-Generated Content**: If the issue/comment is obviously spam, generated by a bot, or something that is not an actual issue to be worked on, add an issue comment with a one-sentence analysis and move to the next issue.
|
||||
|
||||
5. Next, use the GitHub tools to gather additional context about the issue:
|
||||
5. Retrieve the issue content using the `get_issue` tool for any issues that pass the spam checks.
|
||||
|
||||
- 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. Next, use the GitHub tools to gather additional context about the issue:
|
||||
|
||||
6. Analyze the issue content, considering:
|
||||
- Fetch the list of labels available in this repository using the `list_label` tool with `owner: "appwrite"` and `repo: "appwrite"` parameters. This will give you the labels you can use for triaging issues.
|
||||
- Fetch any comments on the issue using the `get_issue_comments` tool to understand recent activity
|
||||
- **Search for duplicate and related issues (repo first, then org-wide)**:
|
||||
- First search in this repository using the `search_issues` tool with a query like: `repo:appwrite/appwrite is:issue (is:open OR is:closed) <key terms>`.
|
||||
- Then perform an org-wide search across the entire Appwrite organization using: `org:appwrite is:issue (is:open OR is:closed) <key terms>`.
|
||||
- Prefer linking to OPEN issues when identifying potential duplicates; include CLOSED ones as related history when useful.
|
||||
|
||||
7. Analyze the issue content, considering:
|
||||
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
|
|
@ -51,35 +78,38 @@ You're a triage assistant for GitHub issues. Your task is to analyze issues crea
|
|||
- 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. 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:
|
||||
9. Select appropriate labels from the available labels list:
|
||||
|
||||
- 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
|
||||
- Search for similar issues. If you find a duplicate of another OPEN issue in THIS repository, you may use a "duplicate" label (if available) and reference the canonical issue.
|
||||
- If the closest match is in another repository within the Appwrite org, do NOT mark as duplicate here; instead, link it in your comment under a "Cross‑repo related issues" section.
|
||||
- Only select labels from the provided list
|
||||
- Don't apply the `good first issue` or `help wanted` labels
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
|
||||
9. Apply the selected labels:
|
||||
10. 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:
|
||||
11. 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)
|
||||
- **If duplicate or related issues were found**, add sections listing them with links:
|
||||
- "### 🔗 Potentially Related Issues (this repo)" – bullet list of same-repo issues with titles and links
|
||||
- If applicable: "### 🌐 Cross-repo related issues (org: appwrite)" – bullet list including `owner/repo#number` with 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.
|
||||
- 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. For bolded section titles, wrap the text with `<strong>` and `</strong>` to make it bold.
|
||||
- Do not indicate/encourage a community member to submit a PR for the issue.
|
||||
|
||||
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.
|
||||
12. After processing all issues, provide a summary of how many issues were triaged (created or updated in the last 24 hours). If no issues matched the criteria, simply note that no issues needed triage.
|
||||
|
|
|
|||
49
README-CN.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner with logo and slogan build like a team of hundreds""></a>
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud""></a>
|
||||
<br />
|
||||
<br />
|
||||
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>
|
||||
|
|
@ -36,7 +36,6 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
|
|||
|
||||
内容:
|
||||
|
||||
|
||||
- [开始](#开始)
|
||||
- [安装](#安装)
|
||||
- [Unix](#unix)
|
||||
|
|
@ -57,7 +56,8 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
|
|||
- [版权说明](#版权说明)
|
||||
|
||||
## 开始
|
||||
要轻松开始使用Appwrite,您可以[**免费注册Appwrite Cloud**](https://cloud.appwrite.io/)。在Appwrite Cloud公开测试版期间,您可以完全免费使用Appwrite,而且我们不会收集您的信用卡信息。
|
||||
|
||||
要轻松开始使用 Appwrite,您可以[**免费注册 Appwrite Cloud**](https://cloud.appwrite.io/)。在 Appwrite Cloud 公开测试版期间,您可以完全免费使用 Appwrite,而且我们不会收集您的信用卡信息。
|
||||
|
||||
## 安装
|
||||
|
||||
|
|
@ -119,22 +119,16 @@ docker run -it --rm `
|
|||
<br /><sub><b>DigitalOcean</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="100" height="100">
|
||||
<a href="https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod">
|
||||
<img width="50" height="39" src="public/images/integrations/gitpod-logo.svg" alt="Gitpod Logo" />
|
||||
<br /><sub><b>Gitpod</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="100" height="100">
|
||||
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
|
||||
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
|
||||
<br /><sub><b>Akamai Compute</b></sub></a>
|
||||
<br /><sub><b>Akamai Compute</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="100" height="100">
|
||||
<a href="https://aws.amazon.com/marketplace/pp/prodview-2hiaeo2px4md6">
|
||||
<img width="50" height="39" src="public/images/integrations/aws-logo.svg" alt="AWS Logo" />
|
||||
<br /><sub><b>AWS Marketplace</b></sub></a>
|
||||
<br /><sub><b>AWS Marketplace</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -188,25 +182,28 @@ docker run -it --rm `
|
|||
以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md)。
|
||||
|
||||
#### 客户端
|
||||
* :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
|
||||
|
||||
- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
|
||||
|
||||
#### 服务器
|
||||
* :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
|
||||
* :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
|
||||
|
||||
- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
|
||||
|
||||
#### 开发者社区
|
||||
* :white_check_mark: [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
|
||||
* :white_check_mark: [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
|
||||
|
||||
- :white_check_mark: [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
|
||||
- :white_check_mark: [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
|
||||
|
||||
找不到需要的的 SDK? - 欢迎通过发起 PR 来帮助我们完善 Appwrite 的软件生态环境 [SDK 生成器](https://github.com/appwrite/sdk-generator)!
|
||||
|
||||
|
|
|
|||
17
README.md
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "Build Like a Team of Hundreds"></a>
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
|
||||
<br />
|
||||
<br />
|
||||
<b>Appwrite is an all-in-one development platform for Web, Mobile, and Flutter applications. Use built-in backend infrastructure and web hosting, all from a single place. Built with the open source community and optimized for developer experience in the coding languages you love.</b>
|
||||
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
|
|
@ -32,13 +32,6 @@ Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend
|
|||
|
||||
Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
|
||||
|
||||
<p align="center">
|
||||
<br />
|
||||
<a href="https://www.producthunt.com/posts/appwrite-2?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-appwrite-2" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=360315&theme=light&period=daily" alt="Appwrite - 100% open source alternative for Firebase | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
Find out more at: [https://appwrite.io](https://appwrite.io).
|
||||
|
|
@ -129,12 +122,6 @@ Choose from one of the providers below:
|
|||
<br /><sub><b>DigitalOcean</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="100" height="100">
|
||||
<a href="https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod">
|
||||
<img width="50" height="39" src="public/images/integrations/gitpod-logo.svg" alt="Gitpod Logo" />
|
||||
<br /><sub><b>Gitpod</b></sub></a>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="100" height="100">
|
||||
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
|
||||
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
|
||||
|
|
|
|||
|
|
@ -688,12 +688,12 @@ return [
|
|||
/** Databases */
|
||||
Exception::DATABASE_NOT_FOUND => [
|
||||
'name' => Exception::DATABASE_NOT_FOUND,
|
||||
'description' => 'Database not found',
|
||||
'description' => 'Database with the requested ID \'%s\' could not be found.',
|
||||
'code' => 404
|
||||
],
|
||||
Exception::DATABASE_ALREADY_EXISTS => [
|
||||
'name' => Exception::DATABASE_ALREADY_EXISTS,
|
||||
'description' => 'Database already exists',
|
||||
'description' => 'Database with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409
|
||||
],
|
||||
Exception::DATABASE_TIMEOUT => [
|
||||
|
|
@ -710,41 +710,41 @@ return [
|
|||
/** Collections */
|
||||
Exception::COLLECTION_NOT_FOUND => [
|
||||
'name' => Exception::COLLECTION_NOT_FOUND,
|
||||
'description' => 'Collection with the requested ID could not be found.',
|
||||
'description' => 'Collection with the requested ID \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::COLLECTION_ALREADY_EXISTS => [
|
||||
'name' => Exception::COLLECTION_ALREADY_EXISTS,
|
||||
'description' => 'A collection with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'description' => 'A collection with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::COLLECTION_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::COLLECTION_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number of collections has been reached.',
|
||||
'description' => 'The maximum number of collections for database \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Tables */
|
||||
Exception::TABLE_NOT_FOUND => [
|
||||
'name' => Exception::TABLE_NOT_FOUND,
|
||||
'description' => 'Table with the requested ID could not be found.',
|
||||
'description' => 'Table with the requested ID \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::TABLE_ALREADY_EXISTS => [
|
||||
'name' => Exception::TABLE_ALREADY_EXISTS,
|
||||
'description' => 'A table with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'description' => 'A table with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::TABLE_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::TABLE_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number of tables has been reached.',
|
||||
'description' => 'The maximum number of tables for database \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Documents */
|
||||
Exception::DOCUMENT_NOT_FOUND => [
|
||||
'name' => Exception::DOCUMENT_NOT_FOUND,
|
||||
'description' => 'Document with the requested ID could not be found.',
|
||||
'description' => 'Document with the requested ID \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::DOCUMENT_INVALID_STRUCTURE => [
|
||||
|
|
@ -764,7 +764,7 @@ return [
|
|||
],
|
||||
Exception::DOCUMENT_ALREADY_EXISTS => [
|
||||
'name' => Exception::DOCUMENT_ALREADY_EXISTS,
|
||||
'description' => 'Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'description' => 'Document with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::DOCUMENT_UPDATE_CONFLICT => [
|
||||
|
|
@ -781,7 +781,7 @@ return [
|
|||
/** Rows */
|
||||
Exception::ROW_NOT_FOUND => [
|
||||
'name' => Exception::ROW_NOT_FOUND,
|
||||
'description' => 'Row with the requested ID could not be found.',
|
||||
'description' => 'Row with the requested ID \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::ROW_INVALID_STRUCTURE => [
|
||||
|
|
@ -791,7 +791,7 @@ return [
|
|||
],
|
||||
Exception::ROW_MISSING_DATA => [
|
||||
'name' => Exception::ROW_MISSING_DATA,
|
||||
'description' => 'The row data is missing. Try again with row data populated',
|
||||
'description' => 'The row data is missing. Try again with row data populated.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ROW_MISSING_PAYLOAD => [
|
||||
|
|
@ -801,7 +801,7 @@ return [
|
|||
],
|
||||
Exception::ROW_ALREADY_EXISTS => [
|
||||
'name' => Exception::ROW_ALREADY_EXISTS,
|
||||
'description' => 'Row with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'description' => 'Row with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::ROW_UPDATE_CONFLICT => [
|
||||
|
|
@ -818,17 +818,17 @@ return [
|
|||
/** Attributes */
|
||||
Exception::ATTRIBUTE_NOT_FOUND => [
|
||||
'name' => Exception::ATTRIBUTE_NOT_FOUND,
|
||||
'description' => 'Attribute with the requested ID could not be found.',
|
||||
'description' => 'Attribute with the requested key \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::ATTRIBUTE_UNKNOWN => [
|
||||
'name' => Exception::ATTRIBUTE_UNKNOWN,
|
||||
'description' => 'The attribute required for the index could not be found. Please confirm all your attributes are in the available state.',
|
||||
'description' => 'The attribute \'%s\' required for the index could not be found. Please confirm all your attributes are in the available state.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ATTRIBUTE_NOT_AVAILABLE => [
|
||||
'name' => Exception::ATTRIBUTE_NOT_AVAILABLE,
|
||||
'description' => 'The requested attribute is not yet available. Please try again later.',
|
||||
'description' => 'The requested attribute \'%s\' is not yet available. Please try again later.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ATTRIBUTE_FORMAT_UNSUPPORTED => [
|
||||
|
|
@ -843,12 +843,12 @@ return [
|
|||
],
|
||||
Exception::ATTRIBUTE_ALREADY_EXISTS => [
|
||||
'name' => Exception::ATTRIBUTE_ALREADY_EXISTS,
|
||||
'description' => 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.',
|
||||
'description' => 'Attribute with the requested key \'%s\' already exists. Attribute keys must be unique, try again with a different key.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number or size of attributes for this collection has been reached.',
|
||||
'description' => 'The maximum number or size of attributes for collection \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ATTRIBUTE_VALUE_INVALID => [
|
||||
|
|
@ -858,7 +858,7 @@ return [
|
|||
],
|
||||
Exception::ATTRIBUTE_TYPE_INVALID => [
|
||||
'name' => Exception::ATTRIBUTE_TYPE_INVALID,
|
||||
'description' => 'The attribute type is invalid.',
|
||||
'description' => 'The attribute \'%s\' type is invalid.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ATTRIBUTE_INVALID_RESIZE => [
|
||||
|
|
@ -869,7 +869,7 @@ return [
|
|||
|
||||
Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED => [
|
||||
'name' => Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED,
|
||||
'description' => 'Attribute type is not supported.',
|
||||
'description' => 'Attribute type \'%s\' is not supported.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
|
|
@ -883,17 +883,17 @@ return [
|
|||
/** Columns */
|
||||
Exception::COLUMN_NOT_FOUND => [
|
||||
'name' => Exception::COLUMN_NOT_FOUND,
|
||||
'description' => 'Column with the requested ID could not be found.',
|
||||
'description' => 'Column with the requested key \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::COLUMN_UNKNOWN => [
|
||||
'name' => Exception::COLUMN_UNKNOWN,
|
||||
'description' => 'The column required for the index could not be found. Please confirm all your columns are in the available state.',
|
||||
'description' => 'The column \'%s\' required for the index could not be found. Please confirm all your columns are in the available state.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::COLUMN_NOT_AVAILABLE => [
|
||||
'name' => Exception::COLUMN_NOT_AVAILABLE,
|
||||
'description' => 'The requested column is not yet available. Please try again later.',
|
||||
'description' => 'The requested column \'%s\' is not yet available. Please try again later.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::COLUMN_FORMAT_UNSUPPORTED => [
|
||||
|
|
@ -908,12 +908,12 @@ return [
|
|||
],
|
||||
Exception::COLUMN_ALREADY_EXISTS => [
|
||||
'name' => Exception::COLUMN_ALREADY_EXISTS,
|
||||
'description' => 'Column with the requested key already exists. Column keys must be unique, try again with a different key.',
|
||||
'description' => 'Column with the requested key \'%s\' already exists. Column keys must be unique, try again with a different key.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::COLUMN_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::COLUMN_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number or size of columns for this table has been reached.',
|
||||
'description' => 'The maximum number or size of columns for table \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::COLUMN_VALUE_INVALID => [
|
||||
|
|
@ -923,7 +923,7 @@ return [
|
|||
],
|
||||
Exception::COLUMN_TYPE_INVALID => [
|
||||
'name' => Exception::COLUMN_TYPE_INVALID,
|
||||
'description' => 'The column type is invalid.',
|
||||
'description' => 'The column \'%s\' type is invalid.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::COLUMN_INVALID_RESIZE => [
|
||||
|
|
@ -933,24 +933,24 @@ return [
|
|||
],
|
||||
Exception::COLUMN_TYPE_NOT_SUPPORTED => [
|
||||
'name' => Exception::COLUMN_TYPE_NOT_SUPPORTED,
|
||||
'description' => 'Column type is not supported.',
|
||||
'description' => 'Column type \'%s\' is not supported.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Indexes */
|
||||
Exception::INDEX_NOT_FOUND => [
|
||||
'name' => Exception::INDEX_NOT_FOUND,
|
||||
'description' => 'Index with the requested ID could not be found.',
|
||||
'description' => 'Index with the requested key \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::INDEX_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::INDEX_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number of indexes has been reached.',
|
||||
'description' => 'The maximum number of indexes for collection \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::INDEX_ALREADY_EXISTS => [
|
||||
'name' => Exception::INDEX_ALREADY_EXISTS,
|
||||
'description' => 'Index with the requested key already exists. Try again with a different key.',
|
||||
'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::INDEX_INVALID => [
|
||||
|
|
@ -960,24 +960,24 @@ return [
|
|||
],
|
||||
Exception::INDEX_DEPENDENCY => [
|
||||
'name' => Exception::INDEX_DEPENDENCY,
|
||||
'description' => 'Attribute cannot be renamed or deleted. Please remove the associated index first.',
|
||||
'description' => 'Attribute \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
|
||||
'code' => 409,
|
||||
],
|
||||
|
||||
/** Column Indexes, same as Indexes but with different type */
|
||||
Exception::COLUMN_INDEX_NOT_FOUND => [
|
||||
'name' => Exception::COLUMN_INDEX_NOT_FOUND,
|
||||
'description' => 'Index with the requested ID could not be found.',
|
||||
'description' => 'Index with the requested key \'%s\' could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number of indexes has been reached.',
|
||||
'description' => 'The maximum number of indexes for table \'%s\' has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::COLUMN_INDEX_ALREADY_EXISTS => [
|
||||
'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS,
|
||||
'description' => 'Index with the requested key already exists. Try again with a different key.',
|
||||
'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::COLUMN_INDEX_INVALID => [
|
||||
|
|
@ -987,19 +987,19 @@ return [
|
|||
],
|
||||
Exception::COLUMN_INDEX_DEPENDENCY => [
|
||||
'name' => Exception::COLUMN_INDEX_DEPENDENCY,
|
||||
'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.',
|
||||
'description' => 'Column \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
|
||||
'code' => 409,
|
||||
],
|
||||
|
||||
/** Transactions */
|
||||
Exception::TRANSACTION_NOT_FOUND => [
|
||||
'name' => Exception::TRANSACTION_NOT_FOUND,
|
||||
'description' => 'Transaction with the requested ID could not be found.',
|
||||
'description' => 'Transaction with the requested ID \'%s\' 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.',
|
||||
'description' => 'Transaction with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::TRANSACTION_INVALID => [
|
||||
|
|
|
|||
25
app/config/platform.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
* Platform configuration
|
||||
*/
|
||||
return [
|
||||
'apiHostname' => System::getEnv('_APP_DOMAIN', 'localhost'),
|
||||
'consoleHostname' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
|
||||
'hostnames' => array_filter(array_unique([
|
||||
System::getEnv('_APP_DOMAIN', 'localhost'),
|
||||
System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'),
|
||||
])),
|
||||
'platformName' => APP_EMAIL_PLATFORM_NAME,
|
||||
'logoUrl' => APP_EMAIL_LOGO_URL,
|
||||
'accentColor' => APP_EMAIL_ACCENT_COLOR,
|
||||
'footerImageUrl' => APP_EMAIL_FOOTER_IMAGE_URL,
|
||||
'twitterUrl' => APP_SOCIAL_TWITTER,
|
||||
'discordUrl' => APP_SOCIAL_DISCORD,
|
||||
'githubUrl' => APP_SOCIAL_GITHUB,
|
||||
'termsUrl' => APP_EMAIL_TERMS_URL,
|
||||
'privacyUrl' => APP_EMAIL_PRIVACY_URL,
|
||||
'websiteUrl' => 'https://' . APP_DOMAIN,
|
||||
];
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
|
|
@ -7,12 +8,8 @@ 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';
|
||||
}
|
||||
$platform = Config::getParam('platform', []);
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
|
||||
$url = $protocol . '://' . $hostname;
|
||||
|
||||
|
|
@ -25,6 +22,8 @@ class UseCases
|
|||
public const DOCUMENTATION = 'documentation';
|
||||
public const BLOG = 'blog';
|
||||
public const AI = 'artificial intelligence';
|
||||
public const FORMS = 'forms';
|
||||
public const DASHBOARD = 'dashboard';
|
||||
}
|
||||
|
||||
const TEMPLATE_FRAMEWORKS = [
|
||||
|
|
@ -1472,4 +1471,135 @@ return [
|
|||
],
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'crm-dashboard-react-admin',
|
||||
'name' => 'CRM dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with CRM features.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [UseCases::DASHBOARD],
|
||||
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/react-admin',
|
||||
'installCommand' => 'pnpm install',
|
||||
'buildCommand' => 'pnpm build && pnpm db-seed',
|
||||
'outputDirectory' => './dist',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.7.*',
|
||||
'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' => 'APPWRITE_API_KEY',
|
||||
'description' => 'Your Appwrite API key (for seeding only)',
|
||||
'value' => '',
|
||||
'placeholder' => 'a0b1...',
|
||||
'required' => true,
|
||||
'type' => 'password'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_DATABASE_ID',
|
||||
'description' => 'Database ID (default: admin)',
|
||||
'value' => 'admin',
|
||||
'placeholder' => 'admin',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_REVIEWS',
|
||||
'description' => 'Table ID for reviews table',
|
||||
'value' => 'reviews',
|
||||
'placeholder' => 'reviews',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_INVOICES',
|
||||
'description' => 'Table ID for invoices table',
|
||||
'value' => 'invoices',
|
||||
'placeholder' => 'invoices',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_ORDERS',
|
||||
'description' => 'Table ID for orders table',
|
||||
'value' => 'orders',
|
||||
'placeholder' => 'orders',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_PRODUCTS',
|
||||
'description' => 'Table ID for products table',
|
||||
'value' => 'products',
|
||||
'placeholder' => 'products',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_CATEGORIES',
|
||||
'description' => 'Table ID for categories table',
|
||||
'value' => 'categories',
|
||||
'placeholder' => 'categories',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => 'VITE_APPWRITE_TABLE_CUSTOMERS',
|
||||
'description' => 'Table ID for customers table',
|
||||
'value' => 'customers',
|
||||
'placeholder' => 'customers',
|
||||
'required' => false,
|
||||
'type' => 'text'
|
||||
],
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'job-applications-formspree',
|
||||
'name' => 'Job applications form with Formspree',
|
||||
'tagline' => 'A simple form submission template using Formspree.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [UseCases::FORMS],
|
||||
'screenshotDark' => $url . '/images/sites/templates/job-applications-formspree-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/job-applications-formspree-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/formspree',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.7.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'VITE_FORMSPREE_FORM_ID',
|
||||
'description' => 'Your Formspree form ID',
|
||||
'value' => '',
|
||||
'placeholder' => 'xrgkpqld',
|
||||
'required' => true,
|
||||
'type' => 'text'
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_DOMAIN',
|
||||
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'.',
|
||||
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'. Multiple domains can be separated by commas.',
|
||||
'introduction' => '',
|
||||
'default' => 'localhost',
|
||||
'required' => true,
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ use Utopia\Emails\Email;
|
|||
use Utopia\Locale\Locale;
|
||||
use Utopia\Storage\Validator\FileName;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
$oauthDefaultSuccess = '/console/auth/oauth2/success';
|
||||
|
|
@ -1283,13 +1283,14 @@ App::get('/v1/account/sessions/oauth2/:provider')
|
|||
->label('abuse-limit', 50)
|
||||
->label('abuse-key', 'ip:{ip}')
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
|
||||
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
|
||||
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
|
||||
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
|
||||
->inject('platform')
|
||||
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project, array $platform) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
|
|
@ -1324,7 +1325,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
|
|||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$host = $platform['consoleHostname'] ?? '';
|
||||
$redirectBase = $protocol . '://' . $host;
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
$redirectBase .= ':' . $port;
|
||||
|
|
@ -1443,7 +1444,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('platforms')
|
||||
->inject('redirectValidator')
|
||||
->inject('devKey')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
|
|
@ -1452,7 +1453,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, array $platforms, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
|
|
@ -1463,7 +1464,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
}
|
||||
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
|
||||
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
|
||||
$redirect = new Redirect($platforms);
|
||||
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
||||
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
|
||||
|
|
@ -1490,11 +1490,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
$state = $defaultState;
|
||||
}
|
||||
|
||||
if ($devKey->isEmpty() && !$redirect->isValid($state['success'])) {
|
||||
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
|
||||
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
|
||||
}
|
||||
|
||||
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirect->isValid($state['failure'])) {
|
||||
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirectValidator->isValid($state['failure'])) {
|
||||
throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
|
||||
}
|
||||
$failure = [];
|
||||
|
|
@ -1945,23 +1945,15 @@ App::get('/v1/account/tokens/oauth2/:provider')
|
|||
->label('abuse-limit', 50)
|
||||
->label('abuse-key', 'ip:{ip}')
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
|
||||
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
|
||||
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
|
||||
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
$callbackBase .= ':' . $port;
|
||||
} elseif ($protocol === 'http' && $port !== '80') {
|
||||
$callbackBase .= ':' . $port;
|
||||
}
|
||||
|
||||
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
|
||||
->inject('platform')
|
||||
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project, array $platform) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
|
||||
$callback = $platform['endpoint'] . '/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
|
||||
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
|
||||
|
||||
if (!$providerEnabled) {
|
||||
|
|
@ -1986,7 +1978,9 @@ App::get('/v1/account/tokens/oauth2/:provider')
|
|||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$host = $platform['consoleHostname'] ?? '';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$redirectBase = $protocol . '://' . $host;
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
$redirectBase .= ':' . $port;
|
||||
|
|
@ -2041,7 +2035,7 @@ App::post('/v1/account/tokens/magic-url')
|
|||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('userId', '', 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. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
|
||||
->param('email', '', new EmailValidator(), 'User email.')
|
||||
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
|
||||
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
|
|
@ -2052,7 +2046,8 @@ App::post('/v1/account/tokens/magic-url')
|
|||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForPassword')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword) {
|
||||
->inject('platform')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
|
|
@ -2158,7 +2153,7 @@ App::post('/v1/account/tokens/magic-url')
|
|||
|
||||
if (empty($url)) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$host = $platform['consoleHostname'] ?? '';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $host;
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
|
|
@ -2985,12 +2980,6 @@ App::get('/v1/account/logs')
|
|||
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 EventAudit($dbForProject);
|
||||
|
||||
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
|
||||
|
|
@ -3469,7 +3458,7 @@ App::post('/v1/account/recovery')
|
|||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('email', '', new EmailValidator(), 'User email.')
|
||||
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey'])
|
||||
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['redirectValidator'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
|
|
@ -3760,11 +3749,12 @@ App::post('/v1/account/verifications/email')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.createEmailVerification'
|
||||
),
|
||||
public: false,
|
||||
)
|
||||
])
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},userId:{userId}')
|
||||
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey']) // TODO add built-in confirm page
|
||||
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['redirectValidator']) // TODO add built-in confirm page
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
|
|
@ -3976,6 +3966,7 @@ App::put('/v1/account/verifications/email')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.updateEmailVerification'
|
||||
),
|
||||
public: false,
|
||||
)
|
||||
])
|
||||
->label('abuse-limit', 10)
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@ App::get('/v1/console/variables')
|
|||
))
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
$validator = new Domain(System::getEnv('_APP_DOMAIN'));
|
||||
$isDomainValid = !empty(System::getEnv('_APP_DOMAIN', '')) && $validator->isKnown() && !$validator->isTest();
|
||||
|
||||
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
|
||||
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
|
||||
|
||||
|
|
@ -55,9 +52,7 @@ App::get('/v1/console/variables')
|
|||
$validator = new IP(IP::V6);
|
||||
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
|
||||
|
||||
$isDomainEnabled = $isDomainValid && (
|
||||
$isAAAAValid || $isAValid || $isCNAMEValid
|
||||
);
|
||||
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
|
||||
|
||||
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
|
||||
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ App::post('/v1/messaging/providers/smtp')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.createSMTPProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -872,6 +873,7 @@ App::post('/v1/messaging/providers/fcm')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.createFCMProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -961,6 +963,7 @@ App::post('/v1/messaging/providers/apns')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.createAPNSProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -1155,12 +1158,6 @@ App::get('/v1/messaging/providers/:providerId/logs')
|
|||
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);
|
||||
$resource = 'provider/' . $providerId;
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
@ -1580,6 +1577,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.updateSMTPProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -2171,6 +2169,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.updateFCMProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -2266,6 +2265,7 @@ App::patch('/v1/messaging/providers/apns/:providerId')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.updateAPNSProvider',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -2562,12 +2562,6 @@ App::get('/v1/messaging/topics/:topicId/logs')
|
|||
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);
|
||||
$resource = 'topic/' . $topicId;
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
@ -2985,12 +2979,6 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
|
|||
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);
|
||||
$resource = 'subscriber/' . $subscriberId;
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
@ -3343,6 +3331,7 @@ App::post('/v1/messaging/messages/sms')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.createSMS',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -3506,7 +3495,8 @@ App::post('/v1/messaging/messages/push')
|
|||
->inject('project')
|
||||
->inject('queueForMessaging')
|
||||
->inject('response')
|
||||
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
|
||||
->inject('platform')
|
||||
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
|
||||
$messageId = $messageId == 'unique()'
|
||||
? ID::unique()
|
||||
: $messageId;
|
||||
|
|
@ -3562,7 +3552,6 @@ App::post('/v1/messaging/messages/push')
|
|||
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$host = System::getEnv('_APP_DOMAIN', 'localhost');
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
|
|
@ -3583,7 +3572,7 @@ App::post('/v1/messaging/messages/push')
|
|||
$image = [
|
||||
'bucketId' => $bucket->getId(),
|
||||
'fileId' => $file->getId(),
|
||||
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
|
||||
'url' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -3785,12 +3774,6 @@ App::get('/v1/messaging/messages/:messageId/logs')
|
|||
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);
|
||||
$resource = 'message/' . $messageId;
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
@ -4192,6 +4175,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'messaging.updateSMS',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'messaging',
|
||||
|
|
@ -4394,7 +4378,8 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
|||
->inject('project')
|
||||
->inject('queueForMessaging')
|
||||
->inject('response')
|
||||
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
|
||||
->inject('platform')
|
||||
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
|
||||
$message = $dbForProject->getDocument('messages', $messageId);
|
||||
|
||||
if ($message->isEmpty()) {
|
||||
|
|
@ -4562,9 +4547,6 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
|||
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$host = System::getEnv('_APP_DOMAIN', 'localhost');
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
if (!\is_null($scheduleTime)) {
|
||||
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
|
||||
|
|
@ -4583,7 +4565,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
|||
$pushData['image'] = [
|
||||
'bucketId' => $bucket->getId(),
|
||||
'fileId' => $file->getId(),
|
||||
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}"
|
||||
'url' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -563,6 +563,7 @@ App::patch('/v1/projects/:projectId/api')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatus',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -620,6 +621,7 @@ App::patch('/v1/projects/:projectId/api/all')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatusAll',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -2024,6 +2026,7 @@ App::patch('/v1/projects/:projectId/smtp')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateSMTP',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -2140,6 +2143,7 @@ App::post('/v1/projects/:projectId/smtp/tests')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.createSMTPTest',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -2234,6 +2238,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.getSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -2400,6 +2405,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
@ -2524,6 +2530,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'projects.deleteSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ use Utopia\Validator\ArrayList;
|
|||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::post('/v1/teams')
|
||||
|
|
@ -486,7 +485,7 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
}
|
||||
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
|
||||
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey']) // TODO add our own built-in confirm page
|
||||
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // TODO add our own built-in confirm page
|
||||
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
|
|
@ -1479,12 +1478,6 @@ App::get('/v1/teams/:teamId/logs')
|
|||
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);
|
||||
$resource = 'team/' . $team->getId();
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
|
|||
|
|
@ -957,11 +957,7 @@ App::get('/v1/users/:userId/logs')
|
|||
} 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 = [];
|
||||
|
|
@ -1817,6 +1813,7 @@ App::patch('/v1/users/:userId/mfa')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.updateMFA',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
@ -1876,6 +1873,7 @@ App::get('/v1/users/:userId/mfa/factors')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.listMFAFactors',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
@ -1934,6 +1932,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.getMFARecoveryCodes',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
@ -1998,6 +1997,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.createMFARecoveryCodes',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
@ -2069,6 +2069,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.updateMFARecoveryCodes',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
@ -2081,7 +2082,8 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
|
|||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_MFA_RECOVERY_CODES,
|
||||
)
|
||||
]
|
||||
],
|
||||
public: false,
|
||||
)
|
||||
])
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
|
|
@ -2140,6 +2142,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type')
|
|||
since: '1.8.0',
|
||||
replaceWith: 'users.deleteMFAAuthenticator',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'users',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use Appwrite\Auth\OAuth2\Github as OAuth2Github;
|
|||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
|
|
@ -77,7 +76,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
|
|||
|
||||
use function Swoole\Coroutine\batch;
|
||||
|
||||
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
|
||||
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, array $platform) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $repository) {
|
||||
try {
|
||||
|
|
@ -133,7 +132,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
|||
|
||||
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
|
||||
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
|
||||
|
||||
|
|
@ -175,7 +174,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
|||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$comment = new Comment();
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
|
||||
|
|
@ -185,7 +184,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
|||
}
|
||||
}
|
||||
} else {
|
||||
$comment = new Comment();
|
||||
$comment = new Comment($platform);
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
|
||||
|
||||
|
|
@ -246,7 +245,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
|||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$comment = new Comment();
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
|
||||
|
|
@ -467,7 +466,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
|||
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
|
||||
if (!empty($previewUrl)) {
|
||||
$comment = new Comment();
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
|
||||
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
|
||||
|
|
@ -542,12 +541,12 @@ App::get('/v1/vcs/github/authorize')
|
|||
type: MethodType::WEBAUTH,
|
||||
hide: true,
|
||||
))
|
||||
->param('success', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a successful installation attempt.', true, ['platforms'])
|
||||
->param('failure', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a failed installation attempt.', true, ['platforms'])
|
||||
->inject('request')
|
||||
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
|
||||
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->action(function (string $success, string $failure, Request $request, Response $response, Document $project) {
|
||||
->inject('platform')
|
||||
->action(function (string $success, string $failure, Response $response, Document $project, array $platform) {
|
||||
$state = \json_encode([
|
||||
'projectId' => $project->getId(),
|
||||
'success' => $success,
|
||||
|
|
@ -556,7 +555,7 @@ App::get('/v1/vcs/github/authorize')
|
|||
|
||||
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
|
||||
if (empty($appName)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
|
||||
|
|
@ -585,10 +584,10 @@ App::get('/v1/vcs/github/callback')
|
|||
->inject('gitHub')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
|
||||
->inject('platform')
|
||||
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Response $response, Database $dbForPlatform, array $platform) {
|
||||
if (empty($state)) {
|
||||
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
|
||||
|
|
@ -615,7 +614,7 @@ App::get('/v1/vcs/github/callback')
|
|||
|
||||
$region = $project->getAttribute('region', 'default');
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
|
||||
$defaultState = [
|
||||
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
|
||||
|
|
@ -1479,8 +1478,9 @@ App::post('/v1/vcs/github/events')
|
|||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->inject('platform')
|
||||
->action(
|
||||
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
|
||||
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$payload = $request->getRawPayload();
|
||||
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
|
||||
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
|
||||
|
|
@ -1523,7 +1523,7 @@ App::post('/v1/vcs/github/events')
|
|||
|
||||
// create new deployment only on push (not committed by us) and not when branch is created or deleted
|
||||
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
}
|
||||
} elseif ($event == $github::EVENT_INSTALLATION) {
|
||||
if ($parsedPayload["action"] == "deleted") {
|
||||
|
|
@ -1579,7 +1579,7 @@ App::post('/v1/vcs/github/events')
|
|||
Query::orderDesc('$createdAt')
|
||||
]));
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
} elseif ($parsedPayload["action"] == "closed") {
|
||||
// Allowed external contributions cleanup
|
||||
|
||||
|
|
@ -1783,13 +1783,13 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
|
|||
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
|
||||
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
|
||||
->inject('gitHub')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
|
||||
->inject('platform')
|
||||
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation->isEmpty()) {
|
||||
|
|
@ -1837,8 +1837,16 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
|
|||
|
||||
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
|
||||
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
|
||||
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
|
||||
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
|
||||
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
|
||||
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
|
||||
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
|
||||
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
|
||||
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
|
||||
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use Appwrite\Event\Event;
|
|||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
|
|
@ -39,6 +39,7 @@ use Utopia\Config\Config;
|
|||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
|
@ -51,33 +52,36 @@ use Utopia\Logger\Log\User;
|
|||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
|
||||
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
if (!empty($previewHostname)) {
|
||||
$host = $previewHostname;
|
||||
}
|
||||
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
|
||||
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
|
||||
} else {
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForPlatform->find('rules', [
|
||||
Query::equal('domain', [$host]),
|
||||
Query::limit(1)
|
||||
])
|
||||
)[0] ?? new Document();
|
||||
}
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($host));
|
||||
}
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$host]),
|
||||
]) ?? new Document();
|
||||
});
|
||||
|
||||
$errorView = __DIR__ . '/../views/general/error.phtml';
|
||||
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$url = $protocol . '://' . $platform['consoleHostname'];
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
|
||||
if ($rule->isEmpty()) {
|
||||
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||
|
|
@ -98,10 +102,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
|||
throw $exception;
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
|
||||
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
|
||||
}
|
||||
if (!in_array($host, $platformHostnames)) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
|
||||
}
|
||||
|
||||
// Act as API - no Proxy logic
|
||||
|
|
@ -143,7 +145,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
|||
|
||||
if ($type === 'deployment') {
|
||||
if (System::getEnv('_APP_OPTIONS_ROUTER_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) {
|
||||
if ($request->getProtocol() !== 'https') {
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
|
||||
}
|
||||
|
|
@ -268,7 +270,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
|||
}
|
||||
|
||||
if (!$authorized) {
|
||||
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$url = $protocol . "://" . $platform['consoleHostname'];
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
|
|
@ -456,14 +458,10 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
|||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_DOMAIN');
|
||||
$endpoint = $protocol . '://' . $hostname . "/v1";
|
||||
|
||||
// Appwrite vars
|
||||
if ($type === 'function') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
|
||||
'APPWRITE_FUNCTION_ID' => $resource->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
|
|
@ -475,7 +473,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
|||
]);
|
||||
} elseif ($type === 'site') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_SITE_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_SITE_API_ENDPOINT' => $platform['endpoint'],
|
||||
'APPWRITE_SITE_ID' => $resource->getId(),
|
||||
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
|
||||
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
|
||||
|
|
@ -845,34 +843,31 @@ App::init()
|
|||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('log')
|
||||
->inject('console')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('platforms')
|
||||
->inject('geodb')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForCertificates')
|
||||
->inject('queueForFunctions')
|
||||
->inject('executor')
|
||||
->inject('platform')
|
||||
->inject('isResourceBlocked')
|
||||
->inject('previewHostname')
|
||||
->inject('devKey')
|
||||
->inject('apiKey')
|
||||
->inject('httpReferrer')
|
||||
->inject('httpReferrerSafe')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
|
||||
->inject('cors')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
$hostname = $request->getHostname() ?? '';
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
|
||||
if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -912,98 +907,12 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
$domain = $request->getHostname();
|
||||
$domains = Config::getParam('domains', []);
|
||||
if (!array_key_exists($domain, $domains)) {
|
||||
$domain = new Domain(!empty($domain) ? $domain : '');
|
||||
|
||||
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
|
||||
$domains[$domain->get()] = false;
|
||||
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
|
||||
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
|
||||
Console::warning('Skipping SSL certificates generation on ACME challenge.');
|
||||
} else {
|
||||
Authorization::disable();
|
||||
|
||||
$envDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
$mainDomain = null;
|
||||
if (!empty($envDomain) && $envDomain !== 'localhost') {
|
||||
$mainDomain = $envDomain;
|
||||
} else {
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
|
||||
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
|
||||
} else {
|
||||
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
|
||||
}
|
||||
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
|
||||
}
|
||||
|
||||
if ($mainDomain !== $domain->get()) {
|
||||
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
|
||||
} else {
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
|
||||
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
|
||||
} else {
|
||||
$domainDocument = $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain->get()])
|
||||
]);
|
||||
}
|
||||
|
||||
$owner = '';
|
||||
$functionsDomainFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
if (!empty($functionsDomainFallback) && \str_ends_with($host, $functionsDomainFallback)) {
|
||||
$functionsDomain = $functionsDomainFallback;
|
||||
}
|
||||
|
||||
if (
|
||||
(!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) ||
|
||||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
|
||||
) {
|
||||
$owner = 'Appwrite';
|
||||
}
|
||||
|
||||
if ($domainDocument->isEmpty()) {
|
||||
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
|
||||
$domainDocument = new Document([
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
'$id' => $ruleId,
|
||||
'domain' => $domain->get(),
|
||||
'type' => 'api',
|
||||
'status' => 'verifying',
|
||||
'projectId' => $console->getId(),
|
||||
'projectInternalId' => $console->getSequence(),
|
||||
'search' => implode(' ', [$ruleId, $domain->get()]),
|
||||
'owner' => $owner,
|
||||
'region' => $console->getAttribute('region')
|
||||
]);
|
||||
|
||||
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
|
||||
$queueForCertificates
|
||||
->setDomain($domainDocument)
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
$domains[$domain->get()] = true;
|
||||
|
||||
Authorization::reset(); // ensure authorization is re-enabled
|
||||
}
|
||||
Config::setParam('domains', $domains);
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
if (\in_array($localeParam, $localeCodes)) {
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
Config::setParam(
|
||||
|
|
@ -1032,8 +941,8 @@ App::init()
|
|||
$warnings = [];
|
||||
|
||||
/*
|
||||
* Response format
|
||||
*/
|
||||
* Response format
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.4.0', '<')) {
|
||||
|
|
@ -1053,14 +962,13 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Security Headers
|
||||
*
|
||||
* As recommended at:
|
||||
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||
*/
|
||||
// Add Appwrite warning headers
|
||||
if (!empty($warnings)) {
|
||||
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
|
||||
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost') { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
|
@ -1068,49 +976,151 @@ App::init()
|
|||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($request->getProtocol() === 'https') {
|
||||
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
|
||||
/**
|
||||
* Security headers
|
||||
*
|
||||
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||
*/
|
||||
App::init()
|
||||
->groups(['api', 'web'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('cors')
|
||||
->inject('devKey')
|
||||
->inject('originValidator')
|
||||
->action(function (Request $request, Response $response, Cors $cors, Document $devKey, Validator $originValidator) {
|
||||
// CORS headers
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
|
||||
// Security headers
|
||||
$response
|
||||
->addHeader('Server', 'Appwrite')
|
||||
->addHeader('X-Content-Type-Options', 'nosniff')
|
||||
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true');
|
||||
->addHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
if (!$devKey->isEmpty()) {
|
||||
$response->addHeader('Access-Control-Allow-Origin', '*');
|
||||
if ($request->getProtocol() === 'https') {
|
||||
$maxAge = 60 * 60 * 24 * 126; // 126 days
|
||||
$response->addHeader('Strict-Transport-Security', "max-age=$maxAge");
|
||||
}
|
||||
|
||||
if (!empty($warnings)) {
|
||||
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
|
||||
// Application level CSRF protection
|
||||
$origin = $request->getOrigin();
|
||||
if (empty($origin) || !$devKey->isEmpty() || !empty($request->getHeader('x-appwrite-key'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication
|
||||
* Skip this check for non-web platforms which are not required to send an origin header
|
||||
*/
|
||||
$origin = $request->getOrigin($request->getReferer(''));
|
||||
$originValidator = new Origin($platforms);
|
||||
|
||||
if (
|
||||
$devKey->isEmpty()
|
||||
&& !empty($origin)
|
||||
&& !$originValidator->isValid($origin)
|
||||
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
|
||||
&& $route->getLabel('origin', false) !== '*'
|
||||
&& empty($request->getHeader('x-appwrite-key', ''))
|
||||
&& \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost'
|
||||
) {
|
||||
$route = $request->getRoute();
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Automatic certificate generation
|
||||
*/
|
||||
App::init()
|
||||
->groups(['api', 'web'])
|
||||
->inject('request')
|
||||
->inject('console')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('platform')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) {
|
||||
$hostname = $request->getHostname();
|
||||
$cache = Config::getParam('hostnames', []);
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
|
||||
// 1. Cache hit
|
||||
if (array_key_exists($hostname, $cache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Domain validation
|
||||
$domain = new Domain(!empty($hostname) ? $hostname : '');
|
||||
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
|
||||
$cache[$domain->get()] = false;
|
||||
Config::setParam('hostnames', $cache);
|
||||
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
|
||||
Console::warning('Skipping SSL certificates generation on ACME challenge.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check if domain is a main domain
|
||||
if (!in_array($domain->get(), $platformHostnames)) {
|
||||
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Check/create rule (requires DB access)
|
||||
Authorization::disable();
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$document = $isMd5
|
||||
? $dbForPlatform->getDocument('rules', md5($domain->get()))
|
||||
: $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain->get()]),
|
||||
]);
|
||||
|
||||
if (!$document->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Create new rule
|
||||
$owner = '';
|
||||
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
|
||||
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
|
||||
$funcDomain = $fallback;
|
||||
}
|
||||
|
||||
if (
|
||||
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
|
||||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
|
||||
) {
|
||||
$owner = 'Appwrite';
|
||||
}
|
||||
|
||||
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
|
||||
$document = new Document([
|
||||
'$id' => $ruleId,
|
||||
'domain' => $domain->get(),
|
||||
'type' => 'api',
|
||||
'status' => 'verifying',
|
||||
'projectId' => $console->getId(),
|
||||
'projectInternalId' => $console->getSequence(),
|
||||
'search' => implode(' ', [$ruleId, $domain->get()]),
|
||||
'owner' => $owner,
|
||||
'region' => $console->getAttribute('region')
|
||||
]);
|
||||
|
||||
$dbForPlatform->createDocument('rules', $document);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
$queueForCertificates
|
||||
->setDomain($document)
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
$cache[$domain->get()] = true;
|
||||
Config::setParam('hostnames', $cache);
|
||||
Authorization::reset();
|
||||
}
|
||||
});
|
||||
|
||||
App::options()
|
||||
->inject('utopia')
|
||||
->inject('swooleRequest')
|
||||
|
|
@ -1125,38 +1135,32 @@ App::options()
|
|||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
->inject('platform')
|
||||
->inject('previewHostname')
|
||||
->inject('project')
|
||||
->inject('devKey')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
|
||||
->inject('cors')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
||||
$origin = $request->getOrigin();
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Server', 'Appwrite')
|
||||
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $origin)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true')
|
||||
->noContent();
|
||||
|
||||
if (!$devKey->isEmpty()) {
|
||||
$response->addHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
|
||||
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
|
||||
*/
|
||||
|
|
@ -1443,18 +1447,16 @@ App::get('/robots.txt')
|
|||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
->inject('platform')
|
||||
->inject('previewHostname')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
|
||||
$host = $request->getHostname() ?? '';
|
||||
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
|
||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
|
||||
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1477,18 +1479,16 @@ App::get('/humans.txt')
|
|||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
->inject('platform')
|
||||
->inject('previewHostname')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
|
||||
$host = $request->getHostname() ?? '';
|
||||
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
|
||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
|
||||
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ use Utopia\Database\Helpers\Role;
|
|||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Host;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
|
@ -27,7 +26,7 @@ App::get('/v1/mock/tests/general/oauth2')
|
|||
->label('docs', false)
|
||||
->label('mock', true)
|
||||
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
|
||||
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.') // Important to deny an open redirect attack
|
||||
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // Important to deny an open redirect attack
|
||||
->param('scope', '', new Text(100), 'OAuth2 scope list.')
|
||||
->param('state', '', new Text(1024), 'OAuth2 state.')
|
||||
->inject('response')
|
||||
|
|
@ -64,7 +63,7 @@ App::get('/v1/mock/tests/general/oauth2/token')
|
|||
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
|
||||
->param('client_secret', '', new Text(100), 'OAuth2 scope list.')
|
||||
->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true)
|
||||
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true)
|
||||
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
|
||||
->param('code', '', new Text(100), 'OAuth2 state.', true)
|
||||
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
|
||||
->inject('response')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use Appwrite\Event\Database as EventDatabase;
|
|||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
|
|
@ -495,6 +497,9 @@ App::init()
|
|||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForMigrations')
|
||||
->inject('dbForProject')
|
||||
->inject('timelimit')
|
||||
->inject('resourceToken')
|
||||
|
|
@ -503,7 +508,8 @@ App::init()
|
|||
->inject('plan')
|
||||
->inject('devKey')
|
||||
->inject('telemetry')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
->inject('platform')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
|
|
@ -577,6 +583,10 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: (@loks0n)
|
||||
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
|
||||
*/
|
||||
/*
|
||||
* Background Jobs
|
||||
*/
|
||||
|
|
@ -607,10 +617,18 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/* Auto-set projects */
|
||||
$queueForDeletes->setProject($project);
|
||||
$queueForDatabase->setProject($project);
|
||||
$queueForBuilds->setProject($project);
|
||||
$queueForMessaging->setProject($project);
|
||||
$queueForFunctions->setProject($project);
|
||||
$queueForBuilds->setProject($project);
|
||||
|
||||
/* Auto-set platforms */
|
||||
$queueForFunctions->setPlatform($platform);
|
||||
$queueForBuilds->setPlatform($platform);
|
||||
$queueForMails->setPlatform($platform);
|
||||
$queueForMigrations->setPlatform($platform);
|
||||
|
||||
// Clone the queues, to prevent events triggered by the database listener
|
||||
// from overwriting the events that are supposed to be triggered in the shutdown hook.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ App::get('/versions')
|
|||
->label('scope', 'public')
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
$platforms = Config::getParam('platforms');
|
||||
$platforms = Config::getParam('sdks');
|
||||
|
||||
$versions = [
|
||||
'server' => APP_VERSION_STABLE,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ 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('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
|
||||
Config::load('platform', __DIR__ . '/../config/platform.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);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,17 @@ use Appwrite\Platform\Modules\Compute\Specification;
|
|||
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
|
||||
// Email
|
||||
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
|
||||
const APP_EMAIL_SECURITY = ''; // Default security email address
|
||||
const APP_EMAIL_LOGO_URL = 'https://cloud.appwrite.io/images/mails/logo.png';
|
||||
const APP_EMAIL_ACCENT_COLOR = '#fd366e';
|
||||
const APP_EMAIL_TERMS_URL = 'https://appwrite.io/terms';
|
||||
const APP_EMAIL_PRIVACY_URL = 'https://appwrite.io/privacy';
|
||||
const APP_EMAIL_PLATFORM_NAME = 'Appwrite';
|
||||
const APP_EMAIL_FOOTER_IMAGE_URL = 'https://appwrite.io/email/footer.png';
|
||||
|
||||
const APP_USERAGENT = APP_NAME . '-Server v%s. Please report abuse at %s';
|
||||
const APP_MODE_DEFAULT = 'default';
|
||||
const APP_MODE_ADMIN = 'admin';
|
||||
|
|
@ -81,7 +86,6 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
|
|||
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
|
||||
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
|
||||
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
|
||||
const APP_HOSTNAME_INTERNAL = 'appwrite';
|
||||
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
|
||||
const APP_COMPUTE_MEMORY_DEFAULT = 512;
|
||||
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ use Appwrite\Event\StatsUsage;
|
|||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
|
|
@ -43,7 +45,6 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
|
|||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
|
|
@ -64,7 +65,7 @@ use Utopia\Storage\Storage;
|
|||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
|
||||
|
||||
|
|
@ -159,79 +160,164 @@ 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, Database $dbForPlatform) {
|
||||
$console->setAttribute('platforms', [ // Always allow current host
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'name' => 'Current Host',
|
||||
'type' => Platform::TYPE_WEB,
|
||||
'hostname' => $request->getHostname(),
|
||||
], Document::SET_TYPE_APPEND);
|
||||
|
||||
$hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', ''));
|
||||
$validator = new Hostname();
|
||||
foreach ($hostnames as $hostname) {
|
||||
$hostname = trim($hostname);
|
||||
if (!$validator->isValid($hostname)) {
|
||||
continue;
|
||||
}
|
||||
$console->setAttribute('platforms', [
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'type' => Platform::TYPE_WEB,
|
||||
'name' => $hostname,
|
||||
'hostname' => $hostname,
|
||||
], Document::SET_TYPE_APPEND);
|
||||
/**
|
||||
* Platform configuration
|
||||
*/
|
||||
App::setResource('platform', function (Request $request) {
|
||||
$platform = Config::getParam('platform', []);
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
|
||||
$port = '';
|
||||
if ($request->getPort() === '443' && $protocol !== 'https') {
|
||||
$port = ':443';
|
||||
}
|
||||
if ($request->getPort() === '80' && $protocol !== 'http') {
|
||||
$port = ':80';
|
||||
}
|
||||
$platform['endpoint'] = "$protocol://{$platform['apiHostname']}{$port}/v1";
|
||||
|
||||
// Add `exp` and `appwrite-callback-{projectId}` schemes
|
||||
return $platform;
|
||||
}, ['request']);
|
||||
|
||||
/**
|
||||
* List of allowed request hostnames for the request.
|
||||
*/
|
||||
App::setResource('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
|
||||
$allowed = [...($platform['hostnames'] ?? [])];
|
||||
|
||||
/* Add platform configured hostnames */
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$project->setAttribute('platforms', [
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'type' => Platform::TYPE_SCHEME,
|
||||
'name' => 'Expo',
|
||||
'key' => 'exp',
|
||||
], Document::SET_TYPE_APPEND);
|
||||
$project->setAttribute('platforms', [
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'type' => Platform::TYPE_SCHEME,
|
||||
'name' => 'Appwrite Callback',
|
||||
'key' => 'appwrite-callback-' . $project->getId(),
|
||||
], Document::SET_TYPE_APPEND);
|
||||
$platforms = $project->getAttribute('platforms', []);
|
||||
$hostnames = Platform::getHostnames($platforms);
|
||||
$allowed = [...$allowed, ...$hostnames];
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
|
||||
if (empty($origin)) {
|
||||
$origin = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
/* Add the request hostname if a dev key is found */
|
||||
if (!$devKey->isEmpty()) {
|
||||
$allowed[] = $request->getHostname();
|
||||
}
|
||||
|
||||
// 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();
|
||||
/* Allow the request origin if a dev key or rule is found */
|
||||
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
|
||||
$allowed[] = $originHostname;
|
||||
}
|
||||
|
||||
return array_unique($allowed);
|
||||
}, ['platform', 'project', 'rule', 'devKey', 'request']);
|
||||
|
||||
/**
|
||||
* List of allowed request schemes for the request.
|
||||
*/
|
||||
App::setResource('allowedSchemes', function (Document $project) {
|
||||
$allowed = [];
|
||||
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
/* Add hardcoded schemes */
|
||||
$allowed[] = 'exp';
|
||||
$allowed[] = 'appwrite-callback-' . $project->getId();
|
||||
|
||||
/* Add platform configured schemes */
|
||||
$platforms = $project->getAttribute('platforms', []);
|
||||
$schemes = Platform::getSchemes($platforms);
|
||||
$allowed = [...$allowed, ...$schemes];
|
||||
}
|
||||
|
||||
return array_unique($allowed);
|
||||
}, ['project']);
|
||||
|
||||
/**
|
||||
* Rule associated with a request origin.
|
||||
*/
|
||||
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project) {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$rule = Authorization::skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($domain));
|
||||
}
|
||||
|
||||
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 $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]) ?? new Document();
|
||||
});
|
||||
|
||||
if ($rule->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
return [
|
||||
...$console->getAttribute('platforms', []),
|
||||
...$project->getAttribute('platforms', []),
|
||||
];
|
||||
}, ['request', 'console', 'project', 'dbForPlatform']);
|
||||
return $rule;
|
||||
}, ['request', 'dbForPlatform', 'project']);
|
||||
|
||||
/**
|
||||
* CORS service
|
||||
*/
|
||||
App::setResource('cors', fn (array $allowedHostnames) => new Cors(
|
||||
$allowedHostnames,
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
allowedHeaders: [
|
||||
'Accept',
|
||||
'Origin',
|
||||
'Cookie',
|
||||
'Set-Cookie',
|
||||
// Content
|
||||
'Content-Type',
|
||||
'Content-Range',
|
||||
// Appwrite
|
||||
'X-Appwrite-Project',
|
||||
'X-Appwrite-Key',
|
||||
'X-Appwrite-Dev-Key',
|
||||
'X-Appwrite-Locale',
|
||||
'X-Appwrite-Mode',
|
||||
'X-Appwrite-JWT',
|
||||
'X-Appwrite-Response-Format',
|
||||
'X-Appwrite-Timeout',
|
||||
'X-Appwrite-ID',
|
||||
'X-Appwrite-Timestamp',
|
||||
'X-Appwrite-Session',
|
||||
// SDK generator
|
||||
'X-SDK-Version',
|
||||
'X-SDK-Name',
|
||||
'X-SDK-Language',
|
||||
'X-SDK-Platform',
|
||||
'X-SDK-GraphQL',
|
||||
// Caching
|
||||
'Range',
|
||||
'Cache-Control',
|
||||
'Expires',
|
||||
'Pragma',
|
||||
// Server to server
|
||||
'X-Fallback-Cookies',
|
||||
'X-Requested-With',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded-User-Agent',
|
||||
],
|
||||
allowCredentials: true,
|
||||
exposedHeaders: [
|
||||
'X-Appwrite-Session',
|
||||
'X-Fallback-Cookies',
|
||||
],
|
||||
), ['allowedHostnames']);
|
||||
|
||||
App::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
|
||||
if (!$devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
return new Origin($allowedHostnames, $allowedSchemes);
|
||||
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
|
||||
|
||||
App::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
|
||||
if (!$devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
return new Redirect($allowedHostnames, $allowedSchemes);
|
||||
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
|
||||
|
||||
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
|
||||
/**
|
||||
|
|
@ -738,7 +824,7 @@ App::setResource('passwordsDictionary', function ($register) {
|
|||
|
||||
|
||||
App::setResource('servers', function () {
|
||||
$platforms = Config::getParam('platforms');
|
||||
$platforms = Config::getParam('sdks');
|
||||
$server = $platforms[APP_PLATFORM_SERVER];
|
||||
|
||||
$languages = array_map(function ($language) {
|
||||
|
|
@ -839,24 +925,6 @@ App::setResource('schema', function ($utopia, $dbForProject) {
|
|||
);
|
||||
}, ['utopia', 'dbForProject']);
|
||||
|
||||
App::setResource('contributors', function () {
|
||||
$path = 'app/config/contributors.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('employees', function () {
|
||||
$path = 'app/config/employees.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('heroes', function () {
|
||||
$path = 'app/config/heroes.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('gitHub', function (Cache $cache) {
|
||||
return new VcsGitHub($cache);
|
||||
}, ['cache']);
|
||||
|
|
@ -923,6 +991,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
|
|||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
}, ['request', 'project', 'servers', 'dbForPlatform']);
|
||||
|
||||
|
|
@ -1063,37 +1132,6 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
|||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request']);
|
||||
|
||||
App::setResource('httpReferrer', function (Request $request): string {
|
||||
$referrer = $request->getReferer();
|
||||
return $referrer;
|
||||
}, ['request']);
|
||||
|
||||
App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $platforms, Database $dbForPlatform, Document $project, App $utopia): string {
|
||||
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
|
||||
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
|
||||
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
|
||||
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
// Safe if route is publicly accessible
|
||||
$route = $utopia->getRoute();
|
||||
if ($route->getLabel('origin', false)) {
|
||||
return $referrer;
|
||||
}
|
||||
|
||||
// Safe if added as web platform
|
||||
$originValidator = new Origin($platforms);
|
||||
if ($originValidator->isValid($request->getOrigin($httpReferrer))) {
|
||||
return $referrer;
|
||||
}
|
||||
|
||||
// Unsafe; Localhost is always safe for ease of local development
|
||||
$origin = 'localhost';
|
||||
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
|
||||
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
|
||||
$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']);
|
||||
|
|
|
|||
|
|
@ -543,7 +543,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$platforms = $app->getResource('platforms');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
|
||||
/*
|
||||
|
|
@ -568,7 +567,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
* Skip this check for non-web platforms which are not required to send an origin header.
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = new Origin($platforms);
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\System\System;
|
||||
|
||||
$development = $this->getParam('development', false);
|
||||
|
|
@ -15,7 +16,8 @@ $labelClass = '';
|
|||
$buttons = [];
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
$platform = Config::getParam('platform', []);
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
// TODO: remove this later
|
||||
if (System::getEnv('_APP_ENV') === 'development') {
|
||||
$hostname = 'localhost';
|
||||
|
|
@ -537,4 +539,4 @@ switch ($type) {
|
|||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -38,9 +38,15 @@ services:
|
|||
depends_on:
|
||||
- appwrite
|
||||
networks:
|
||||
- gateway
|
||||
- appwrite
|
||||
- runtimes
|
||||
appwrite:
|
||||
aliases:
|
||||
- appwrite.test
|
||||
gateway:
|
||||
aliases:
|
||||
- appwrite.test
|
||||
runtimes:
|
||||
aliases:
|
||||
- appwrite.test
|
||||
|
||||
appwrite:
|
||||
container_name: appwrite
|
||||
|
|
@ -1084,13 +1090,26 @@ services:
|
|||
# GraphQl Explorer - A nice UI for exploring GraphQL API
|
||||
|
||||
maildev: # used mainly for dev tests
|
||||
image: appwrite/mailcatcher:1.0.0
|
||||
image: appwrite/mailcatcher:1.1.1
|
||||
container_name: appwrite-mailcatcher
|
||||
<<: *x-logging
|
||||
ports:
|
||||
- "9503:1080"
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
|
||||
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
|
||||
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
|
||||
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
|
||||
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
|
||||
- "traefik.http.routers.appwrite_maildev_https.tls=true"
|
||||
|
||||
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
|
||||
image: appwrite/requestcatcher:1.0.0
|
||||
|
|
@ -1119,14 +1138,54 @@ services:
|
|||
- 9506:8080
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- ADMINER_DESIGN=pepa-linha
|
||||
- ADMINER_DEFAULT_SERVER=mariadb
|
||||
- ADMINER_DEFAULT_USERNAME=root
|
||||
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
|
||||
- ADMINER_DEFAULT_DB=appwrite
|
||||
configs:
|
||||
- source: adminer-index.php
|
||||
target: /var/www/html/index.php
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.tls=true"
|
||||
|
||||
redis-insight:
|
||||
image: redis/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- REDIS_HOSTS=redis
|
||||
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
|
||||
configs:
|
||||
- source: redisinsight-connections.json
|
||||
target: /mnt/connections.json
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
|
||||
ports:
|
||||
- "8081:5540"
|
||||
|
||||
|
|
@ -1151,6 +1210,61 @@ networks:
|
|||
runtimes:
|
||||
name: runtimes
|
||||
|
||||
configs:
|
||||
redisinsight-connections.json:
|
||||
content: |
|
||||
[
|
||||
{
|
||||
"compressor": "NONE",
|
||||
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
|
||||
"host": "redis",
|
||||
"port": 6379,
|
||||
"name": "redis:6379",
|
||||
"db": 0,
|
||||
"username": "default",
|
||||
"password": null,
|
||||
"connectionType": "STANDALONE",
|
||||
"nameFromProvider": null,
|
||||
"provider": "REDIS",
|
||||
"lastConnection": "2025-10-16T09:22:02.591Z",
|
||||
"modules": [
|
||||
{
|
||||
"name": "ReJSON",
|
||||
"version": 20808,
|
||||
"semanticVersion": "2.8.8"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"version": 21015,
|
||||
"semanticVersion": "2.10.15"
|
||||
}
|
||||
],
|
||||
"tls": false,
|
||||
"tlsServername": null,
|
||||
"verifyServerCert": null,
|
||||
"caCert": null,
|
||||
"clientCert": null,
|
||||
"ssh": false,
|
||||
"sshOptions": null,
|
||||
"forceStandalone": false,
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
|
||||
adminer-index.php:
|
||||
content: |
|
||||
<?php
|
||||
if(!count($$_GET)) {
|
||||
$$_POST['auth'] = [
|
||||
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
|
||||
'driver' => 'server', /* seems to autodetect the driver from server settings */
|
||||
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
|
||||
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
|
||||
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
|
||||
];
|
||||
}
|
||||
include './adminer.php';
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
appwrite-redis:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 724 KiB After Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 548 KiB After Width: | Height: | Size: 291 KiB |
BIN
public/images/sites/templates/crm-dashboard-react-admin-dark.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
|
@ -115,7 +115,8 @@ class Build extends Event
|
|||
'resource' => $this->resource,
|
||||
'deployment' => $this->deployment,
|
||||
'type' => $this->type,
|
||||
'template' => $this->template
|
||||
'template' => $this->template,
|
||||
'platform' => $this->platform
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +131,7 @@ class Build extends Event
|
|||
$this->resource = null;
|
||||
$this->deployment = null;
|
||||
$this->template = null;
|
||||
$this->platform = [];
|
||||
parent::reset();
|
||||
|
||||
return $this;
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ class Event
|
|||
protected array $sensitive = [];
|
||||
protected array $payload = [];
|
||||
protected array $context = [];
|
||||
protected array $platform = [];
|
||||
protected ?Document $project = null;
|
||||
protected ?Document $user = null;
|
||||
protected ?string $userId = null;
|
||||
|
||||
protected bool $paused = false;
|
||||
|
||||
/** @var bool Non-critical events will not throw an exception when enqueuing of the event fails. */
|
||||
|
|
@ -153,6 +155,28 @@ class Event
|
|||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set platform for this event.
|
||||
*
|
||||
* @param array $platform
|
||||
* @return self
|
||||
*/
|
||||
public function setPlatform(array $platform): self
|
||||
{
|
||||
$this->platform = $platform;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform for this event.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPlatform(): array
|
||||
{
|
||||
return $this->platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user for this event.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ class Func extends Event
|
|||
/**
|
||||
* Sets custom headers for the function event.
|
||||
*
|
||||
* @param string $headers
|
||||
* @param array $headers
|
||||
* @return self
|
||||
*/
|
||||
public function setHeaders(array $headers): self
|
||||
|
|
@ -217,6 +217,7 @@ class Func extends Event
|
|||
'path' => $this->path,
|
||||
'headers' => $this->headers,
|
||||
'method' => $this->method,
|
||||
'platform' => $this->platform
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class Migration extends Event
|
|||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'migration' => $this->migration,
|
||||
'platform' => $this->platform,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,7 +389,8 @@ class Exception extends \Exception
|
|||
string $message = null,
|
||||
int|string $code = null,
|
||||
\Throwable $previous = null,
|
||||
?string $view = null
|
||||
?string $view = null,
|
||||
array $params = []
|
||||
) {
|
||||
$this->errors = Config::getParam('errors');
|
||||
$this->type = $type;
|
||||
|
|
@ -405,7 +406,13 @@ class Exception extends \Exception
|
|||
}
|
||||
}
|
||||
|
||||
$this->message = $message ?? $this->errors[$type]['description'];
|
||||
// Format message with params if provided
|
||||
if (!empty($params) && $message === null) {
|
||||
$description = $this->errors[$type]['description'] ?? '';
|
||||
$this->message = !empty($description) ? sprintf($description, ...$params) : '';
|
||||
} else {
|
||||
$this->message = $message ?? $this->errors[$type]['description'];
|
||||
}
|
||||
|
||||
$this->publish = $this->errors[$type]['publish'] ?? ($this->code >= 500);
|
||||
|
||||
|
|
|
|||
88
src/Appwrite/Network/Cors.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Network;
|
||||
|
||||
/**
|
||||
* Generate CORS response headers for an incoming request.
|
||||
*
|
||||
* Allowed origins are matched by hostname only. Arrays passed to the
|
||||
* constructor (methods, headers, exposed headers) are formatted into
|
||||
* comma-separated header strings.
|
||||
*/
|
||||
final class Cors
|
||||
{
|
||||
public const string HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
|
||||
public const string HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods';
|
||||
public const string HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers';
|
||||
public const string HEADER_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
|
||||
public const string HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers';
|
||||
public const string HEADER_MAX_AGE = 'Access-Control-Max-Age';
|
||||
|
||||
/**
|
||||
* @param array<string> $allowedHosts Array of allowed hosts
|
||||
* @param array<string> $allowedMethods Array of allowed methods
|
||||
* @param array<string> $allowedHeaders Array of allowed header
|
||||
* @param array<string> $exposedHeaders Array of exposed headers
|
||||
* @param bool $allowCredentials Whether to allow credentials (default: false)
|
||||
* @param int $maxAge Maximum age of the preflight response (default: 86400 seconds)
|
||||
*/
|
||||
public function __construct(
|
||||
private array $allowedHosts,
|
||||
private array $allowedMethods,
|
||||
private array $allowedHeaders,
|
||||
private array $exposedHeaders,
|
||||
private bool $allowCredentials = false,
|
||||
private int $maxAge = 86400,
|
||||
) {
|
||||
$this->allowedHosts = \array_map('strtolower', $this->allowedHosts);
|
||||
|
||||
if ($this->allowedHosts === ['*'] && $allowCredentials === true) {
|
||||
throw new \InvalidArgumentException(
|
||||
'CORS invariant violated: cannot use wildcard origin "*" when credentials are enabled.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CORS headers for a given request origin.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function headers(string $origin): array
|
||||
{
|
||||
$headers = [
|
||||
self::HEADER_ALLOW_METHODS => implode(', ', $this->allowedMethods),
|
||||
self::HEADER_ALLOW_HEADERS => implode(', ', $this->allowedHeaders),
|
||||
self::HEADER_EXPOSE_HEADERS => implode(', ', $this->exposedHeaders),
|
||||
self::HEADER_ALLOW_CREDENTIALS => $this->allowCredentials ? 'true' : 'false',
|
||||
self::HEADER_MAX_AGE => $this->maxAge,
|
||||
];
|
||||
|
||||
// Wildcard allow-all
|
||||
if ($this->allowedHosts === ['*']) {
|
||||
$headers[self::HEADER_ALLOW_ORIGIN] = $origin;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
// Normal origin handling
|
||||
$origin = strtolower(trim($origin));
|
||||
if ($origin === '') {
|
||||
return $headers;
|
||||
}
|
||||
|
||||
$host = parse_url($origin, PHP_URL_HOST);
|
||||
if (!\is_string($host) || $host === '') {
|
||||
return $headers;
|
||||
}
|
||||
|
||||
// Match only by host
|
||||
if (!\in_array($host, $this->allowedHosts, true)) {
|
||||
return $headers;
|
||||
}
|
||||
|
||||
// Accepted
|
||||
$headers[self::HEADER_ALLOW_ORIGIN] = $origin;
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ use Utopia\Validator\Hostname;
|
|||
|
||||
class Origin extends Validator
|
||||
{
|
||||
protected array $hostnames = [];
|
||||
protected array $schemes = [];
|
||||
protected ?string $scheme = null;
|
||||
protected ?string $host = null;
|
||||
protected string $origin = '';
|
||||
|
|
@ -17,12 +15,11 @@ class Origin extends Validator
|
|||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<\Utopia\Database\Document> $platforms
|
||||
* @param array<string> $allowedHostnames
|
||||
* @param array<string> $allowedSchemes
|
||||
*/
|
||||
public function __construct(array $platforms)
|
||||
public function __construct(protected array $allowedHostnames, protected array $allowedSchemes)
|
||||
{
|
||||
$this->hostnames = Platform::getHostnames($platforms);
|
||||
$this->schemes = Platform::getSchemes($platforms);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -53,11 +50,11 @@ class Origin extends Validator
|
|||
Platform::SCHEME_EDGE_EXTENSION,
|
||||
];
|
||||
if (in_array($this->scheme, $webPlatforms, true)) {
|
||||
$validator = new Hostname($this->hostnames);
|
||||
$validator = new Hostname($this->allowedHostnames);
|
||||
return $validator->isValid($this->host);
|
||||
}
|
||||
|
||||
if (!empty($this->scheme) && in_array($this->scheme, $this->schemes, true)) {
|
||||
if (!empty($this->scheme) && in_array($this->scheme, $this->allowedSchemes, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class Create extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.createMFAAuthenticator',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class Delete extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.deleteMFAAuthenticator',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class Update extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.updateMFAAuthenticator',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ class Create extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.createMFAChallenge',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
@ -332,6 +333,8 @@ class Create extends Action
|
|||
->setParam('userId', $user->getId())
|
||||
->setParam('challengeId', $challenge->getId());
|
||||
|
||||
$response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($challenge, Response::MODEL_MFA_CHALLENGE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class Update extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.updateMFAChallenge',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
@ -109,7 +110,7 @@ class Update extends Action
|
|||
$recoveryCodeChallenge = function (Document $challenge, Document $user, string $otp) use ($dbForProject) {
|
||||
if (
|
||||
$challenge->isSet('type') &&
|
||||
$challenge->getAttribute('type') === \strtolower(Type::RECOVERY_CODE)
|
||||
$challenge->getAttribute('type') === Type::RECOVERY_CODE
|
||||
) {
|
||||
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
|
||||
if (\in_array($otp, $mfaRecoveryCodes)) {
|
||||
|
|
@ -131,7 +132,7 @@ class Update extends Action
|
|||
Type::TOTP => Challenge\TOTP::challenge($challenge, $user, $otp),
|
||||
Type::PHONE => Challenge\Phone::challenge($challenge, $user, $otp),
|
||||
Type::EMAIL => Challenge\Email::challenge($challenge, $user, $otp),
|
||||
\strtolower(Type::RECOVERY_CODE) => $recoveryCodeChallenge($challenge, $user, $otp),
|
||||
Type::RECOVERY_CODE => $recoveryCodeChallenge($challenge, $user, $otp),
|
||||
default => false
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class XList extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.listMFAFactors',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class Create extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.createMFARecoveryCodes',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
@ -100,6 +101,8 @@ class Create extends Action
|
|||
'recoveryCodes' => $mfaRecoveryCodes
|
||||
]);
|
||||
|
||||
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Get extends Action
|
|||
since: '1.8.0',
|
||||
replaceWith: 'account.getMFARecoveryCodes',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
|
|
|||
|
|
@ -235,8 +235,9 @@ class Base extends Action
|
|||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$ruleId = $isMd5 ? md5($domain) : ID::unique();
|
||||
|
||||
Authorization::skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class Get extends Action
|
|||
->param('type', '', new WhiteList(['rules']), 'Resource type.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('platform')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +67,10 @@ class Get extends Action
|
|||
string $value,
|
||||
string $type,
|
||||
Response $response,
|
||||
Database $dbForPlatform
|
||||
Database $dbForPlatform,
|
||||
array $platform
|
||||
) {
|
||||
$domains = $platform['hostnames'] ?? [];
|
||||
if ($type === 'rules') {
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
|
|
@ -89,13 +92,7 @@ class Get extends Action
|
|||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
|
||||
}
|
||||
|
||||
$deniedDomains = [
|
||||
'localhost',
|
||||
APP_HOSTNAME_INTERNAL
|
||||
];
|
||||
|
||||
$mainDomain = System::getEnv('_APP_DOMAIN', '');
|
||||
$deniedDomains[] = $mainDomain;
|
||||
$deniedDomains = [...$domains];
|
||||
|
||||
if (!empty($sitesDomain)) {
|
||||
$deniedDomains[] = $sitesDomain;
|
||||
|
|
|
|||
|
|
@ -307,19 +307,19 @@ abstract class Action extends UtopiaAction
|
|||
$options = $attribute->getAttribute('options', []);
|
||||
|
||||
if (in_array($type, Database::SPATIAL_TYPES) && !$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
|
||||
throw new Exception($this->getSpatialTypeNotSupportedException());
|
||||
throw new Exception($this->getSpatialTypeNotSupportedException(), params: [$type]);
|
||||
}
|
||||
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if (!empty($format)) {
|
||||
|
|
@ -382,9 +382,9 @@ abstract class Action extends UtopiaAction
|
|||
$dbForProject->checkAttribute($collection, $attribute);
|
||||
$attribute = $dbForProject->createDocument('attributes', $attribute);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$key]);
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$collectionId]);
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception($this->getStructureException(), $e->getMessage());
|
||||
} catch (Throwable $e) {
|
||||
|
|
@ -426,9 +426,9 @@ abstract class Action extends UtopiaAction
|
|||
$dbForProject->checkAttribute($relatedCollection, $twoWayAttribute);
|
||||
$dbForProject->createDocument('attributes', $twoWayAttribute);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$twoWayKey]);
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$relatedCollection->getId()]);
|
||||
} catch (StructureException) {
|
||||
throw new Exception($this->getStructureException());
|
||||
} catch (Throwable $e) {
|
||||
|
|
@ -477,19 +477,19 @@ abstract class Action extends UtopiaAction
|
|||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
|
||||
|
||||
if ($attribute->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$key]);
|
||||
}
|
||||
|
||||
if ($attribute->getAttribute('status') !== 'available') {
|
||||
|
|
@ -590,7 +590,7 @@ abstract class Action extends UtopiaAction
|
|||
} catch (IndexException) {
|
||||
throw new Exception(Exception::INDEX_INVALID);
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$collectionId]);
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
|
|
@ -624,11 +624,11 @@ abstract class Action extends UtopiaAction
|
|||
newKey: $newKey ?? null
|
||||
);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$key]);
|
||||
} catch (IndexException $e) {
|
||||
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$collectionId]);
|
||||
} catch (TruncateException) {
|
||||
throw new Exception($this->getInvalidResizeException());
|
||||
}
|
||||
|
|
@ -644,7 +644,7 @@ abstract class Action extends UtopiaAction
|
|||
try {
|
||||
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$newKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -74,17 +74,17 @@ class Delete extends Action
|
|||
{
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
|
||||
if ($attribute->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$key]);
|
||||
}
|
||||
|
||||
$validator = new IndexDependencyValidator(
|
||||
|
|
@ -93,7 +93,7 @@ class Delete extends Action
|
|||
);
|
||||
|
||||
if (!$validator->isValid($attribute)) {
|
||||
throw new Exception($this->getIndexDependencyException());
|
||||
throw new Exception($this->getIndexDependencyException(), params: [$key]);
|
||||
}
|
||||
|
||||
if ($attribute->getAttribute('status') === 'available') {
|
||||
|
|
@ -108,12 +108,12 @@ class Delete extends Action
|
|||
if ($options['twoWay']) {
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $options['relatedCollection']);
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$options['relatedCollection']]);
|
||||
}
|
||||
|
||||
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $options['twoWayKey']);
|
||||
if ($relatedAttribute->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$options['twoWayKey']]);
|
||||
}
|
||||
|
||||
if ($relatedAttribute->getAttribute('status') === 'available') {
|
||||
|
|
|
|||
|
|
@ -75,17 +75,17 @@ class Get extends Action
|
|||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$attribute = $dbForProject->getDocument('attributes', $database->getSequence() . '_' . $collection->getSequence() . '_' . $key);
|
||||
if ($attribute->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$key]);
|
||||
}
|
||||
|
||||
$type = $attribute->getAttribute('type');
|
||||
|
|
|
|||
|
|
@ -89,23 +89,24 @@ class Create extends Action
|
|||
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
|
||||
{
|
||||
$key ??= $relatedCollectionId;
|
||||
$twoWayKeyWasProvided = $twoWayKey !== null;
|
||||
$twoWayKey ??= $collectionId;
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$relatedCollectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId);
|
||||
$relatedCollection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $relatedCollectionDocument->getSequence());
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$relatedCollectionId]);
|
||||
}
|
||||
|
||||
$attributes = $collection->getAttribute('attributes', []);
|
||||
|
|
@ -115,14 +116,17 @@ class Create extends Action
|
|||
}
|
||||
|
||||
if (\strtolower($attribute->getId()) === \strtolower($key)) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$key]);
|
||||
}
|
||||
|
||||
if (
|
||||
\strtolower($attribute->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) &&
|
||||
$attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId()
|
||||
) {
|
||||
throw new Exception($this->getDuplicateException(), 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.');
|
||||
// If user explicitly provided twoWayKey, report that.
|
||||
// Otherwise report the key that they're trying to create.
|
||||
$conflictingKey = $twoWayKeyWasProvided ? $twoWayKey : $key;
|
||||
throw new Exception($this->getDuplicateException(), params: [$conflictingKey]);
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -71,12 +71,12 @@ class XList extends Action
|
|||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class Create extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collectionId = $collectionId === 'unique()' ? ID::unique() : $collectionId;
|
||||
|
|
@ -113,11 +113,11 @@ class Create extends Action
|
|||
'search' => \implode(' ', [$collectionId, $name]),
|
||||
]));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$collectionId]);
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$databaseId]);
|
||||
} catch (NotFoundException) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
|
@ -205,13 +205,13 @@ class Create extends Action
|
|||
);
|
||||
} catch (DuplicateException) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$collectionId]);
|
||||
} catch (IndexException $e) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
|
||||
} catch (LimitException) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getLimitException());
|
||||
throw new Exception($this->getLimitException(), params: [$collectionId]);
|
||||
} catch (\Throwable $e) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw $e;
|
||||
|
|
@ -227,7 +227,7 @@ class Create extends Action
|
|||
}
|
||||
} catch (DuplicateException) {
|
||||
$this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId());
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$collectionId]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId());
|
||||
throw $e;
|
||||
|
|
|
|||
|
|
@ -71,12 +71,12 @@ class Delete extends Action
|
|||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('database_' . $database->getSequence(), $collectionId)) {
|
||||
|
|
|
|||
|
|
@ -95,12 +95,12 @@ class Decrement extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
|
|
|
|||
|
|
@ -95,12 +95,12 @@ class Increment extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
|
|
|
|||
|
|
@ -88,12 +88,12 @@ class Delete extends Action
|
|||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$hasRelationships = \array_filter(
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ class Update extends Action
|
|||
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if ($transactionId === null) {
|
||||
|
|
|
|||
|
|
@ -90,12 +90,12 @@ class Upsert extends Action
|
|||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$hasRelationships = \array_filter(
|
||||
|
|
@ -180,7 +180,7 @@ class Upsert extends Action
|
|||
} catch (ConflictException) {
|
||||
throw new Exception($this->getConflictException());
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: ['multiple']);
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
|
|
|
|||
|
|
@ -187,12 +187,12 @@ class Create extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$hasRelationships = \array_filter(
|
||||
|
|
@ -372,7 +372,7 @@ class Create extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
|
|
@ -444,9 +444,9 @@ class Create extends Action
|
|||
)
|
||||
);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$documentId]);
|
||||
} catch (NotFoundException) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
|
|
|
|||
|
|
@ -105,13 +105,13 @@ class Delete extends Action
|
|||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
// Read permission should not be required for delete
|
||||
|
|
@ -125,7 +125,7 @@ class Delete extends Action
|
|||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$documentId]);
|
||||
}
|
||||
|
||||
// Handle transaction staging
|
||||
|
|
@ -134,7 +134,7 @@ class Delete extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ class Get extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -114,7 +114,7 @@ class Get extends Action
|
|||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$documentId]);
|
||||
}
|
||||
|
||||
$operations = 0;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use Appwrite\Utopia\Response as UtopiaResponse;
|
|||
use MaxMind\Db\Reader;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
|
|
@ -80,17 +79,17 @@ class XList extends Action
|
|||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$documentId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -99,12 +98,6 @@ class XList extends Action
|
|||
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);
|
||||
$type = $this->getCollectionsEventsContext();
|
||||
$context = $this->getContext();
|
||||
|
|
|
|||
|
|
@ -104,13 +104,13 @@ class Update extends Action
|
|||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if ($transactionId === null) {
|
||||
|
|
@ -129,7 +129,7 @@ class Update extends Action
|
|||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$documentId]);
|
||||
}
|
||||
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
|
|
@ -252,7 +252,7 @@ class Update extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
|
|
@ -326,7 +326,7 @@ class Update extends Action
|
|||
} catch (ConflictException) {
|
||||
throw new Exception($this->getConflictException());
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$documentId]);
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
|
|
|
|||
|
|
@ -111,12 +111,12 @@ class Upsert extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
if ($transactionId === null) {
|
||||
|
|
@ -262,7 +262,7 @@ class Upsert extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
|
|
@ -335,7 +335,7 @@ class Upsert extends Action
|
|||
} catch (ConflictException) {
|
||||
throw new Exception($this->getConflictException());
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$documentId]);
|
||||
} catch (RelationshipException $e) {
|
||||
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
|
||||
} catch (StructureException $e) {
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ class XList extends Action
|
|||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -65,13 +65,13 @@ class Get extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$response->dynamic($collection, $this->getResponseModel());
|
||||
|
|
|
|||
|
|
@ -87,14 +87,14 @@ class Create extends Action
|
|||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
// table or collection.
|
||||
throw new Exception($this->getGrandParentNotFoundException());
|
||||
throw new Exception($this->getGrandParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$count = $dbForProject->count('indexes', [
|
||||
|
|
@ -105,7 +105,7 @@ class Create extends Action
|
|||
$limit = $dbForProject->getLimitForIndexes();
|
||||
|
||||
if ($count >= $limit) {
|
||||
throw new Exception($this->getLimitException(), 'Index limit exceeded');
|
||||
throw new Exception($this->getLimitException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$oldAttributes = \array_map(
|
||||
|
|
@ -148,7 +148,7 @@ class Create extends Action
|
|||
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
|
||||
|
||||
if ($attributeIndex === false) {
|
||||
throw new Exception($this->getParentUnknownException(), "Unknown $contextType: " . $attribute . ". Verify the $contextType name or create the $contextType.");
|
||||
throw new Exception($this->getParentUnknownException(), params: [$attribute]);
|
||||
}
|
||||
|
||||
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
|
||||
|
|
@ -160,8 +160,7 @@ class Create extends Action
|
|||
}
|
||||
|
||||
if ($attributeStatus !== 'available') {
|
||||
$contextType = ucfirst($contextType);
|
||||
throw new Exception($this->getParentNotAvailableException(), "$contextType not available: " . $oldAttributes[$attributeIndex]['key']);
|
||||
throw new Exception($this->getParentNotAvailableException(), params: [$oldAttributes[$attributeIndex]['key']]);
|
||||
}
|
||||
|
||||
if (empty($lengths[$i])) {
|
||||
|
|
@ -209,7 +208,7 @@ class Create extends Action
|
|||
try {
|
||||
$index = $dbForProject->createDocument('indexes', $index);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
throw new Exception($this->getDuplicateException(), params: [$key]);
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
|
|
|||
|
|
@ -78,19 +78,19 @@ class Delete extends Action
|
|||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
// table or collection.
|
||||
throw new Exception($this->getGrandParentNotFoundException());
|
||||
throw new Exception($this->getGrandParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$index = $dbForProject->getDocument('indexes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
|
||||
|
||||
if (empty($index->getId())) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$key]);
|
||||
}
|
||||
|
||||
// Only update status if removing available index
|
||||
|
|
|
|||
|
|
@ -67,18 +67,18 @@ class Get extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
// table or collection.
|
||||
throw new Exception($this->getGrandParentNotFoundException());
|
||||
throw new Exception($this->getGrandParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$index = $collection->find('key', $key, 'indexes');
|
||||
if (empty($index)) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$key]);
|
||||
}
|
||||
|
||||
$response->dynamic($index, $this->getResponseModel());
|
||||
|
|
|
|||
|
|
@ -75,14 +75,14 @@ class XList extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
// table or collection.
|
||||
throw new Exception($this->getGrandParentNotFoundException());
|
||||
throw new Exception($this->getGrandParentNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use DeviceDetector\DeviceDetector as Detector;
|
|||
use MaxMind\Db\Reader;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
|
|
@ -80,14 +79,14 @@ class XList extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -96,12 +95,6 @@ class XList extends Action
|
|||
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);
|
||||
$context = $this->getContext();
|
||||
$resource = "database/$databaseId/$context/$collectionId";
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ class Update extends Action
|
|||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$permissions ??= $collection->getPermissions();
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class Get extends Action
|
|||
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception($this->getNotFoundException());
|
||||
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
|
||||
}
|
||||
|
||||
$periods = Config::getParam('usage', []);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class XList extends Action
|
|||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class Create extends Action
|
|||
'type' => $this->getDatabaseType(),
|
||||
]));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::DATABASE_ALREADY_EXISTS, params: [$databaseId]);
|
||||
} catch (StructureException $e) {
|
||||
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
|
||||
}
|
||||
|
|
@ -109,13 +109,13 @@ class Create extends Action
|
|||
try {
|
||||
$dbForProject->createCollection('database_' . $database->getSequence(), $attributes, $indexes);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
|
||||
} catch (IndexException) {
|
||||
throw new Exception(Exception::DATABASE_ALREADY_EXISTS, params: [$databaseId]);
|
||||
} catch (IndexException $e) {
|
||||
throw new Exception(Exception::INDEX_INVALID);
|
||||
} catch (LimitException) {
|
||||
// TODO: @Jake, how do we handle this collection/table?
|
||||
// there's no context awareness at this level on what the api is.
|
||||
throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED);
|
||||
throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('databaseId', $database->getId());
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class Delete extends Action
|
|||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('databases', $databaseId)) {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class Get extends Action
|
|||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$response->dynamic($database, UtopiaResponse::MODEL_DATABASE);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use DeviceDetector\DeviceDetector as Detector;
|
|||
use MaxMind\Db\Reader;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
|
@ -76,7 +75,7 @@ class XList extends Action
|
|||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -85,12 +84,6 @@ class XList extends Action
|
|||
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);
|
||||
$resource = 'database/' . $databaseId;
|
||||
$logs = $audit->getLogsByResource($resource, $queries);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class Delete extends Action
|
|||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('transactions', $transactionId);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class Get extends Action
|
|||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
|
||||
$response
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class Create extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
|
|
@ -115,14 +115,14 @@ class Create extends Action
|
|||
|
||||
$database = $databases[$operation['databaseId']] ??= Authorization::skip(fn () => $dbForProject->getDocument('databases', $operation['databaseId']));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$operation['databaseId']]);
|
||||
}
|
||||
|
||||
$collection = $collections[$operation[$this->getGroupId()]] ??=
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()]));
|
||||
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND, params: [$operation[$this->getGroupId()]]);
|
||||
}
|
||||
|
||||
if (\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
|
|
@ -148,7 +148,7 @@ class Create extends Action
|
|||
|
||||
$document = $transactionState->getDocument($collectionKey, $documentId, $transactionId);
|
||||
if ($document->isEmpty() && !$isDependant && $operation['action'] !== 'upsert') {
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, params: [$documentId]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class Update extends Action
|
|||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND, params: [$transactionId]);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_READY);
|
||||
|
|
@ -135,9 +135,10 @@ class Update extends Action
|
|||
$operations = [];
|
||||
$totalOperations = 0;
|
||||
$databaseOperations = [];
|
||||
$currentDocumentId = null;
|
||||
|
||||
try {
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) {
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
|
|
@ -156,6 +157,7 @@ class Update extends Action
|
|||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
$documentId = $operation['documentId'];
|
||||
$currentDocumentId = $documentId;
|
||||
$createdAt = new \DateTime($operation['$createdAt']);
|
||||
$action = $operation['action'];
|
||||
$data = $operation['data'];
|
||||
|
|
@ -244,7 +246,8 @@ class Update extends Action
|
|||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e);
|
||||
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e, params: [$currentDocumentId ?? 'unknown']);
|
||||
} catch (DuplicateException | ConflictException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class Update extends Action
|
|||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$database = $dbForProject->updateDocument('databases', $databaseId, $database
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class Get extends Action
|
|||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
}
|
||||
|
||||
$periods = Config::getParam('usage', []);
|
||||
|
|
|
|||