diff --git a/.gitignore b/.gitignore index 03e6949..49887dd 100644 --- a/.gitignore +++ b/.gitignore @@ -306,6 +306,7 @@ infra/docker/supabase/volumes/storage/ # OpenCode local config # .opencode/ is now tracked - see .opencode/.gitignore for exclusions +.opencode/opencode.json midscene_run/ # Local git worktrees diff --git a/.opencode/opencode.json b/.opencode/opencode.json.example similarity index 92% rename from .opencode/opencode.json rename to .opencode/opencode.json.example index d571d20..63f11c1 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json.example @@ -17,7 +17,7 @@ "supabase": { "type": "remote", "enabled": true, - "url": "http://localhost:8001/mcp" + "url": "" } } } diff --git a/.opencode/opencode.json.old b/.opencode/opencode.json.old deleted file mode 100644 index 8cd9d3c..0000000 --- a/.opencode/opencode.json.old +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "supabase": { - "type": "local", - "enabled": true, - "command": [ - "npx", - "-y", - "@aliyun-rds/supabase-mcp-server", - "--supabase-url", - "http://47.112.66.83", - "--supabase-anon-key", - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzczMDI3NDE5LCJleHAiOjEzMjgzNjY3NDE5fQ.NVXDla5_nYPdcJk_81fc3k1UrnNTrNne_trMqt6Hg4g", - "--supabase-service-role-key", - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJzZXJ2aWNlX3JvbGUiLCJpYXQiOjE3NzMwMjc0MTksImV4cCI6MTMyODM2Njc0MTl9.RzQBia-3QcjupsHnqaxgDWB7wnY9R7Ms9R8pMokyvLY" - ] - } - } -} diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json new file mode 100644 index 0000000..28ed456 --- /dev/null +++ b/.opencode/package-lock.json @@ -0,0 +1,376 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.14.22" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.14.22", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.22.tgz", + "integrity": "sha512-lJlukegf5ECEHm9Y0NxCjXNfUArpPSUHP6hc+M4VCJ3NFk8uzzVsIXAzPS9Hvf2ltzjEYD/ulCOTi6pleeZ6yw==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.14.22", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.99", + "@opentui/solid": ">=0.1.99" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.14.22", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.22.tgz", + "integrity": "sha512-1PjkrZRAwm9ocfTwOleP/e31HYtLVODb2E1hYTRHMmvF2rmAdCm7lztguYVkAPn/B6koGpFvhslTQH7j+38Fjw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", + "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/lib/features/settings/presentation/screens/coin_center_screen.dart b/apps/lib/features/settings/presentation/screens/coin_center_screen.dart index 7e53597..04ff07a 100644 --- a/apps/lib/features/settings/presentation/screens/coin_center_screen.dart +++ b/apps/lib/features/settings/presentation/screens/coin_center_screen.dart @@ -108,7 +108,7 @@ class _CoinCenterScreenState extends State { _packages = result.packages; }); } - } catch (e, stackTrace) { + } catch (e) { _logger.warning( message: 'Failed to reload packages after purchase', extra: {'error': e.toString()}, diff --git a/apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart b/apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart index 0a5932b..106d30b 100644 --- a/apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart +++ b/apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart @@ -445,74 +445,6 @@ class CoinPackageCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.xl), ), - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.xs), - Text( - l10n.settingsCoinAmount(amount), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - if (badge != null) - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, - ), - decoration: BoxDecoration( - color: palette.historyGoldBg, - borderRadius: BorderRadius.circular(AppRadius.full), - ), - child: Text( - badge!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: palette.historyGoldText, - fontWeight: FontWeight.w700, - ), - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - if (!isAvailable && unavailableMessage != null) - Text( - unavailableMessage!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colors.error, - ), - ) - else - Row( - children: [ - Text( - price, - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith(color: colors.primary), - ), - const Spacer(), - FilledButton( - onPressed: isPurchasing || !isAvailable ? null : onPurchase, - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - ), - ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 7b0ada7..e37e4c2 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -13,6 +13,7 @@ This file governs `backend/**` only. Keep it minimal, enforceable, and non-dupli - Python commands must use `uv` (`uv run`, `uv add`). - Backend startup/shutdown must use `./infra/scripts/app.sh`. - Check runtime logs from `./logs/*.log`. +- Docker Compose usage: see `infra/docker/README.md` for environment-based service activation. ## Code Quality Baseline diff --git a/backend/src/core/config/settings.py b/backend/src/core/config/settings.py index 77b38ba..edb9458 100644 --- a/backend/src/core/config/settings.py +++ b/backend/src/core/config/settings.py @@ -189,8 +189,8 @@ class SensitiveWordSettings(BaseModel): class TestSettings(BaseModel): - phone: str = "" - password: str = "" + email: str = "" + code: str = "" class TaskiqSettings(BaseModel): diff --git a/backend/src/v1/auth/gateway.py b/backend/src/v1/auth/gateway.py index 76a6c4d..ece45d3 100644 --- a/backend/src/v1/auth/gateway.py +++ b/backend/src/v1/auth/gateway.py @@ -78,15 +78,18 @@ class SupabaseAuthGateway(AuthServiceGateway): async def create_email_session( self, request: EmailSessionCreateRequest ) -> SessionResponse: - if config.runtime.environment == "dev": - return await create_dev_email_session( - request=request, - client=self._get_client(), - admin_client=self._get_admin_client(), - auth_unavailable_detail=AUTH_UNAVAILABLE_DETAIL, - is_auth_upstream_unavailable=_is_auth_upstream_unavailable, - map_auth_response=_map_auth_response, - ) + test_email = config.test.email.strip() + test_code = config.test.code.strip() + if test_email and test_code: + if request.email == test_email and request.token == test_code: + return await create_dev_email_session( + request=request, + client=self._get_client(), + admin_client=self._get_admin_client(), + auth_unavailable_detail=AUTH_UNAVAILABLE_DETAIL, + is_auth_upstream_unavailable=_is_auth_upstream_unavailable, + map_auth_response=_map_auth_response, + ) client = self._get_client() payload: dict[str, Any] = { diff --git a/backend/src/v1/payments/service.py b/backend/src/v1/payments/service.py index 8ea32f1..2a48ff4 100644 --- a/backend/src/v1/payments/service.py +++ b/backend/src/v1/payments/service.py @@ -434,19 +434,16 @@ class PaymentService: return try: - import jwt as pyjwt - parts = signed_payload.split(".") if len(parts) < 2: logger.warning("Malformed Apple notification signed_payload") return - payload_bytes = parts[1] + "=" * (-len(parts[1]) % 4) import base64 - - decoded = base64.urlsafe_b64decode(payload_bytes) import json + payload_bytes = parts[1] + "=" * (-len(parts[1]) % 4) + decoded = base64.urlsafe_b64decode(payload_bytes) notification_data: Any = json.loads(decoded) except Exception: logger.exception("Failed to decode Apple server notification payload") diff --git a/backend/src/v1/points/service.py b/backend/src/v1/points/service.py index 820dee1..c16d1bc 100644 --- a/backend/src/v1/points/service.py +++ b/backend/src/v1/points/service.py @@ -500,9 +500,9 @@ class PointsService: id=str(row.id), direction=row.direction, amount=row.amount, - balance_after=row.balance_after, - change_type=row.change_type, - created_at=row.created_at.isoformat(), + balanceAfter=row.balance_after, + changeType=row.change_type, + createdAt=row.created_at.isoformat(), ) ) diff --git a/backend/tests/integration/payments/test_verify_flow.py b/backend/tests/integration/payments/test_verify_flow.py index 880f5a9..5eae655 100644 --- a/backend/tests/integration/payments/test_verify_flow.py +++ b/backend/tests/integration/payments/test_verify_flow.py @@ -1,5 +1,3 @@ -from __future__ import annotations - """ Integration tests for Apple IAP payment verify flow. @@ -7,6 +5,8 @@ Prerequisite: backend must be running via `./infra/scripts/app.sh restart`. These tests hit the live HTTP API against the test database. """ +from __future__ import annotations + import pytest diff --git a/backend/tests/unit/payments/__init__.py b/backend/tests/unit/payments/__init__.py index a96f1cd..1d3cf25 100644 --- a/backend/tests/unit/payments/__init__.py +++ b/backend/tests/unit/payments/__init__.py @@ -6,7 +6,6 @@ import json from v1.payments.apple_verifier import ( AppleJwsVerifier, VerificationError, - VerifiedTransaction, ) diff --git a/backend/tests/unit/payments/test_payment_service.py b/backend/tests/unit/payments/test_payment_service.py index c9a539d..a25c8fa 100644 --- a/backend/tests/unit/payments/test_payment_service.py +++ b/backend/tests/unit/payments/test_payment_service.py @@ -571,7 +571,6 @@ class TestHandleServerNotificationRefund: verifier=_FakeVerifier(result=_make_verified_transaction()), ) - import base64 import json signed_txn = _make_fake_signed_transaction(transaction_id="2000000999999001") diff --git a/backend/tests/unit/test_auth_gateway_dev_session.py b/backend/tests/unit/test_auth_gateway_dev_session.py index 0555817..61ae770 100644 --- a/backend/tests/unit/test_auth_gateway_dev_session.py +++ b/backend/tests/unit/test_auth_gateway_dev_session.py @@ -10,7 +10,7 @@ from v1.auth.schemas import AuthUser, EmailSessionCreateRequest, SessionResponse @pytest.mark.asyncio -async def test_create_email_session_uses_dev_bypass( +async def test_create_email_session_uses_test_account_bypass( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = SupabaseAuthGateway() @@ -28,7 +28,8 @@ async def test_create_email_session_uses_dev_bypass( calls.update(kwargs) return expected - monkeypatch.setattr(gateway_module.config.runtime, "environment", "dev") + monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com") + monkeypatch.setattr(gateway_module.config.test, "code", "123456") monkeypatch.setattr( gateway_module, "create_dev_email_session", _fake_create_dev_email_session ) @@ -47,7 +48,7 @@ async def test_create_email_session_uses_dev_bypass( @pytest.mark.asyncio -async def test_create_email_session_uses_verify_otp_in_non_dev( +async def test_create_email_session_uses_verify_otp_when_test_account_not_configured( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = SupabaseAuthGateway() @@ -66,7 +67,8 @@ async def test_create_email_session_uses_verify_otp_in_non_dev( user=SimpleNamespace(id="user-2", email="test@example.com"), ) - monkeypatch.setattr(gateway_module.config.runtime, "environment", "prod") + monkeypatch.setattr(gateway_module.config.test, "email", "") + monkeypatch.setattr(gateway_module.config.test, "code", "") monkeypatch.setattr( gateway, "_get_client", @@ -81,3 +83,79 @@ async def test_create_email_session_uses_verify_otp_in_non_dev( "token": "123456", } assert response.user.email == "test@example.com" + + +@pytest.mark.asyncio +async def test_create_email_session_uses_verify_otp_when_email_mismatch( + monkeypatch: pytest.MonkeyPatch, +) -> None: + gateway = SupabaseAuthGateway() + request = EmailSessionCreateRequest(email="other@example.com", token="123456") + captured_payload: dict[str, str] = {} + + def _verify_otp(payload: dict[str, str]) -> SimpleNamespace: + captured_payload.update(payload) + return SimpleNamespace( + session=SimpleNamespace( + access_token="access", + refresh_token="refresh", + expires_in=3600, + token_type="bearer", + ), + user=SimpleNamespace(id="user-3", email="other@example.com"), + ) + + monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com") + monkeypatch.setattr(gateway_module.config.test, "code", "123456") + monkeypatch.setattr( + gateway, + "_get_client", + lambda: SimpleNamespace(auth=SimpleNamespace(verify_otp=_verify_otp)), + ) + + response = await gateway.create_email_session(request) + + assert captured_payload == { + "type": "email", + "email": "other@example.com", + "token": "123456", + } + assert response.user.email == "other@example.com" + + +@pytest.mark.asyncio +async def test_create_email_session_uses_verify_otp_when_code_mismatch( + monkeypatch: pytest.MonkeyPatch, +) -> None: + gateway = SupabaseAuthGateway() + request = EmailSessionCreateRequest(email="test@example.com", token="654321") + captured_payload: dict[str, str] = {} + + def _verify_otp(payload: dict[str, str]) -> SimpleNamespace: + captured_payload.update(payload) + return SimpleNamespace( + session=SimpleNamespace( + access_token="access", + refresh_token="refresh", + expires_in=3600, + token_type="bearer", + ), + user=SimpleNamespace(id="user-4", email="test@example.com"), + ) + + monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com") + monkeypatch.setattr(gateway_module.config.test, "code", "123456") + monkeypatch.setattr( + gateway, + "_get_client", + lambda: SimpleNamespace(auth=SimpleNamespace(verify_otp=_verify_otp)), + ) + + response = await gateway.create_email_session(request) + + assert captured_payload == { + "type": "email", + "email": "test@example.com", + "token": "654321", + } + assert response.user.email == "test@example.com" diff --git a/backend/tests/unit/test_parse_profile_settings.py b/backend/tests/unit/test_parse_profile_settings.py index 612ff33..1d7ab11 100644 --- a/backend/tests/unit/test_parse_profile_settings.py +++ b/backend/tests/unit/test_parse_profile_settings.py @@ -17,7 +17,6 @@ class TestParseProfileSettings: "preferences": { "language": "en-US", "timezone": "America/New_York", - "country": "US", }, "privacy": {"profile_visibility": "private"}, "notification": { @@ -32,7 +31,6 @@ class TestParseProfileSettings: assert isinstance(result.preferences, PreferenceSettings) assert result.preferences.language == "en-US" assert result.preferences.timezone == "America/New_York" - assert result.preferences.country == "US" assert isinstance(result.notification, NotificationSettings) assert result.notification.allow_notifications is True assert result.notification.allow_vibration is False @@ -45,7 +43,6 @@ class TestParseProfileSettings: assert isinstance(result.preferences, PreferenceSettings) assert result.preferences.language == "zh-CN" assert result.preferences.timezone == "Asia/Shanghai" - assert result.preferences.country == "US" assert isinstance(result.notification, NotificationSettings) assert result.notification.allow_notifications is True assert result.notification.allow_vibration is True @@ -60,7 +57,6 @@ class TestParseProfileSettings: assert result.preferences.language == "en-US" assert result.preferences.timezone == "Asia/Shanghai" - assert result.preferences.country == "US" def test_parse_profile_settings_with_partial_notification(self) -> None: raw = { @@ -104,21 +100,11 @@ class TestParseProfileSettings: with pytest.raises(ValueError, match="timezone must be a valid IANA timezone"): parse_profile_settings(raw) - def test_parse_profile_settings_country_normalized_to_uppercase(self) -> None: - raw = { - "preferences": { - "country": "us", - }, - } - result = parse_profile_settings(raw) - assert result.preferences.country == "US" - def test_profile_settings_v1_model_dump(self) -> None: settings = ProfileSettingsV1( preferences=PreferenceSettings( language="en-US", timezone="UTC", - country="US", ), notification=NotificationSettings( allow_notifications=True, diff --git a/infra/docker/README.md b/infra/docker/README.md new file mode 100644 index 0000000..e49c8d6 --- /dev/null +++ b/infra/docker/README.md @@ -0,0 +1,126 @@ +# Docker Compose Infrastructure + +> Local development infrastructure orchestration. + +--- + +## Overview + +This directory contains Docker Compose configurations for local development: + +- `docker-compose.yml` - Base services (Redis, etc.) +- `supabase/docker-compose.yml` - Local Supabase stack (PostgreSQL, Auth, Storage, Studio, etc.) + +--- + +## Environment-Based Service Activation + +Services are conditionally enabled based on `ERYAO_RUNTIME__ENVIRONMENT`: + +| Environment | Services | +|-------------|----------| +| `dev` | Redis + Local Supabase | +| `prod` | Redis only (use cloud Supabase) | + +### How It Works + +Supabase services use `profiles: [dev]`. They only start when the `dev` profile is activated. + +--- + +## Usage + +### Development (with local Supabase) + +```bash +cd infra/docker +docker compose --profile dev up -d +``` + +Or with environment variable: + +```bash +ERYAO_RUNTIME__ENVIRONMENT=dev docker compose --profile dev up -d +``` + +### Production (cloud Supabase) + +```bash +cd infra/docker +docker compose up -d +``` + +--- + +## Services Reference + +### Base Services (always started) + +| Service | Port | Description | +|---------|------|-------------| +| Redis | 6379 | Caching and queue backend | + +### Dev-Only Services (profile: dev) + +| Service | Port | Description | +|---------|------|-------------| +| PostgreSQL | 5432 | Local database | +| GoTrue (Auth) | 9999 | Authentication service | +| PostgREST | 3000 | REST API for database | +| Storage | 5000 | File storage service | +| Studio | 3000 | Supabase dashboard UI | +| Kong | 8001, 8443 | API gateway | + +--- + +## Configuration + +All services read configuration from `.env` (symlinked to project root `.env`). + +Required environment variables: + +```bash +# Runtime +ERYAO_RUNTIME__ENVIRONMENT=dev + +# Database +ERYAO_DATABASE__PORT=5432 +ERYAO_DATABASE__NAME=postgres +ERYAO_DATABASE__PASSWORD=your-password + +# Supabase +ERYAO_SUPABASE__JWT_SECRET=your-jwt-secret +ERYAO_SUPABASE__ANON_KEY=your-anon-key +ERYAO_SUPABASE__SERVICE_ROLE_KEY=your-service-role-key +ERYAO_SUPABASE__PUBLIC_URL=http://localhost:8001 + +# Redis +ERYAO_REDIS__PORT=6379 +ERYAO_REDIS__PASSWORD=your-redis-password +``` + +--- + +## Troubleshooting + +### Supabase services not starting + +1. Check profile is activated: `docker compose --profile dev config` +2. Verify environment variables in `.env` +3. Check logs: `docker compose logs ` + +### Port conflicts + +If ports are already in use, override in `.env`: + +```bash +ERYAO_DATABASE__PORT=5433 +ERYAO_REDIS__PORT=6380 +``` + +### Reset local database + +```bash +docker compose --profile dev down -v +docker compose --profile dev up -d +``` diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index e89e523..d6f002a 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -4,29 +4,29 @@ include: - ./supabase/docker-compose.yml services: - redis: - image: redis:7.4.2-alpine - container_name: eryao-local-redis - restart: unless-stopped - ports: - - "127.0.0.1:${ERYAO_REDIS__PORT:-6379}:6379" - volumes: - - redis_data:/data - environment: - REDIS_PASSWORD: ${ERYAO_REDIS__PASSWORD:-} - command: > - sh -c 'if [ -n "$$REDIS_PASSWORD" ]; then redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"; else redis-server --appendonly yes; fi' - healthcheck: - test: - [ - "CMD", - "sh", - "-c", - "if [ -n \"$$REDIS_PASSWORD\" ]; then redis-cli -a \"$$REDIS_PASSWORD\" ping; else redis-cli ping; fi", - ] - interval: 5s - timeout: 3s - retries: 5 + redis: + image: redis:7.4.2-alpine + container_name: eryao-local-redis + restart: unless-stopped + ports: + - "127.0.0.1:${ERYAO_REDIS__PORT:-6379}:6379" + volumes: + - redis_data:/data + environment: + REDIS_PASSWORD: ${ERYAO_REDIS__PASSWORD:-} + command: > + sh -c 'if [ -n "$$REDIS_PASSWORD" ]; then redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"; else redis-server --appendonly yes; fi' + healthcheck: + test: + [ + "CMD", + "sh", + "-c", + 'if [ -n "$$REDIS_PASSWORD" ]; then redis-cli -a "$$REDIS_PASSWORD" ping; else redis-cli ping; fi', + ] + interval: 5s + timeout: 3s + retries: 5 volumes: - redis_data: + redis_data: diff --git a/infra/docker/supabase/docker-compose.yml b/infra/docker/supabase/docker-compose.yml index 3d3c929..b7815e2 100644 --- a/infra/docker/supabase/docker-compose.yml +++ b/infra/docker/supabase/docker-compose.yml @@ -2,6 +2,7 @@ name: eryao-supabase services: db: + profiles: [dev] container_name: eryao-supabase-db image: supabase/postgres:15.8.1.085 restart: unless-stopped @@ -33,6 +34,7 @@ services: - 127.0.0.1:${ERYAO_DATABASE__PORT:-5432}:5432 auth: + profiles: [dev] container_name: eryao-supabase-auth image: supabase/gotrue:v2.186.0 restart: unless-stopped @@ -73,6 +75,7 @@ services: GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify rest: + profiles: [dev] container_name: eryao-supabase-rest image: postgrest/postgrest:v14.8 restart: unless-stopped @@ -91,6 +94,7 @@ services: PGRST_APP_SETTINGS_JWT_EXP: 3600 storage: + profiles: [dev] container_name: eryao-supabase-storage image: supabase/storage-api:v1.48.26 restart: unless-stopped @@ -124,6 +128,7 @@ services: - storage-data:/var/lib/storage meta: + profiles: [dev] container_name: eryao-supabase-meta image: supabase/postgres-meta:v0.96.3 restart: unless-stopped @@ -145,6 +150,7 @@ services: retries: 1 studio: + profiles: [dev] container_name: eryao-supabase-studio image: supabase/studio:2026.04.08-sha-205cbe7 restart: unless-stopped @@ -180,6 +186,7 @@ services: - studio-functions:/var/lib/functions kong: + profiles: [dev] container_name: eryao-supabase-kong image: kong/kong:3.9.1 restart: unless-stopped diff --git a/infra/scripts/app.sh b/infra/scripts/app.sh index b10670e..6cd30ef 100755 --- a/infra/scripts/app.sh +++ b/infra/scripts/app.sh @@ -20,6 +20,7 @@ load_env_if_exists() { # shellcheck disable=SC1090 . "$ENV_LOADER" load_env_file "$ENV_FILE" + load_env_file "$ROOT_DIR/.env.local" } is_port_in_use() { diff --git a/infra/scripts/dev-migrate.sh b/infra/scripts/dev-migrate.sh index 2b00643..d548287 100755 --- a/infra/scripts/dev-migrate.sh +++ b/infra/scripts/dev-migrate.sh @@ -26,6 +26,7 @@ fi # shellcheck disable=SC1090 . "$ENV_LOADER" load_env_file "$ENV_FILE" +load_env_file "$ROOT_DIR/.env.local" cd "$ROOT_DIR"