From bffdb3858b8dad5255cef5f49703304498b153b3 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 7 Oct 2026 09:27:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E7=A1=80=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=90=AD=E5=BB=BA=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 8 +-- .gitignore | 6 +- AGENTS.md | 1 + Makefile | 20 +++--- README.md | 32 ++++++--- {services/api/api => api}/openapi.yaml | 0 {services/api/cmd => cmd}/api/main.go | 28 ++++---- {services/api/cmd => cmd}/db/main.go | 4 +- {services/api/cmd => cmd}/user/main.go | 10 +-- docs/development-state.md | 19 +++++ services/api/go.mod => go.mod | 2 +- services/api/go.sum => go.sum | 0 go.work | 3 - go.work.sum | 32 --------- .../platform => internal}/adminui/dist/.keep | 0 .../platform => internal}/adminui/handler.go | 0 .../platform => internal}/apperror/errors.go | 0 .../platform => internal}/config/config.go | 0 .../config/config_test.go | 0 .../database/database.go | 3 +- .../database/database_test.go | 0 .../database/migrations/000001_foundation.sql | 0 .../migrations/000002_identity_account.sql | 0 .../http => internal/handler}/identity.go | 16 ++--- .../handler}/identity_test.go | 11 ++- .../http => internal/handler}/router.go | 9 +-- .../http => internal/handler}/router_test.go | 4 +- .../integration/identity_account_test.go | 22 +++--- .../domain => internal/model}/account.go | 2 +- .../domain => internal/model}/auth.go | 2 +- .../domain => internal/model}/cash_entry.go | 2 +- .../domain => internal/model}/money.go | 5 +- .../domain => internal/model}/money_test.go | 2 +- .../model}/schema_migration.go | 2 +- .../domain => internal/model}/user.go | 2 +- .../platform => internal}/ratelimit/redis.go | 0 .../repository/account.go | 57 ++++++--------- internal/repository/errors.go | 24 +++++++ .../repository/identity.go | 58 ++++++--------- internal/service/account.go | 70 +++++++++++++++++++ .../identity => internal}/service/auth.go | 51 +++++++------- .../service/auth_test.go | 26 +++---- .../operations => internal}/service/health.go | 0 package-lock.json | 52 +++++++------- package.json | 2 +- .../api/internal/portfolio/service/account.go | 69 ------------------ {apps => web}/admin/components.json | 0 {apps => web}/admin/index.html | 0 {apps => web}/admin/package.json | 0 {apps => web}/admin/scripts/embed.mjs | 2 +- {apps => web}/admin/src/App.tsx | 0 {apps => web}/admin/src/Console.tsx | 0 .../admin/src/components/UsersPanel.tsx | 0 .../admin/src/components/ui/badge.tsx | 0 .../admin/src/components/ui/button.tsx | 0 .../admin/src/components/ui/card.tsx | 0 .../admin/src/components/ui/input.tsx | 0 {apps => web}/admin/src/index.css | 0 {apps => web}/admin/src/lib/api.ts | 0 {apps => web}/admin/src/lib/auth.ts | 0 {apps => web}/admin/src/lib/utils.ts | 0 {apps => web}/admin/src/main.tsx | 0 {apps => web}/admin/tsconfig.json | 0 {apps => web}/admin/vite.config.ts | 0 64 files changed, 326 insertions(+), 332 deletions(-) rename {services/api/api => api}/openapi.yaml (100%) rename {services/api/cmd => cmd}/api/main.go (60%) rename {services/api/cmd => cmd}/db/main.go (91%) rename {services/api/cmd => cmd}/user/main.go (85%) rename services/api/go.mod => go.mod (98%) rename services/api/go.sum => go.sum (100%) delete mode 100644 go.work delete mode 100644 go.work.sum rename {services/api/internal/platform => internal}/adminui/dist/.keep (100%) rename {services/api/internal/platform => internal}/adminui/handler.go (100%) rename {services/api/internal/platform => internal}/apperror/errors.go (100%) rename {services/api/internal/platform => internal}/config/config.go (100%) rename {services/api/internal/platform => internal}/config/config_test.go (100%) rename {services/api/internal/platform => internal}/database/database.go (98%) rename {services/api/internal/platform => internal}/database/database_test.go (100%) rename {services/api/internal/platform => internal}/database/migrations/000001_foundation.sql (100%) rename {services/api/internal/platform => internal}/database/migrations/000002_identity_account.sql (100%) rename {services/api/internal/transport/http => internal/handler}/identity.go (95%) rename {services/api/internal/transport/http => internal/handler}/identity_test.go (77%) rename {services/api/internal/transport/http => internal/handler}/router.go (90%) rename {services/api/internal/transport/http => internal/handler}/router_test.go (97%) rename {services/api/internal => internal}/integration/identity_account_test.go (87%) rename {services/api/internal/portfolio/domain => internal/model}/account.go (96%) rename {services/api/internal/identity/domain => internal/model}/auth.go (97%) rename {services/api/internal/portfolio/domain => internal/model}/cash_entry.go (96%) rename {services/api/internal/portfolio/domain => internal/model}/money.go (92%) rename {services/api/internal/portfolio/domain => internal/model}/money_test.go (97%) rename {services/api/internal/platform/database => internal/model}/schema_migration.go (94%) rename {services/api/internal/identity/domain => internal/model}/user.go (96%) rename {services/api/internal/platform => internal}/ratelimit/redis.go (100%) rename services/api/internal/portfolio/repository/postgres.go => internal/repository/account.go (57%) create mode 100644 internal/repository/errors.go rename services/api/internal/identity/repository/postgres.go => internal/repository/identity.go (62%) create mode 100644 internal/service/account.go rename {services/api/internal/identity => internal}/service/auth.go (70%) rename {services/api/internal/identity => internal}/service/auth_test.go (76%) rename {services/api/internal/operations => internal}/service/health.go (100%) delete mode 100644 services/api/internal/portfolio/service/account.go rename {apps => web}/admin/components.json (100%) rename {apps => web}/admin/index.html (100%) rename {apps => web}/admin/package.json (100%) rename {apps => web}/admin/scripts/embed.mjs (83%) rename {apps => web}/admin/src/App.tsx (100%) rename {apps => web}/admin/src/Console.tsx (100%) rename {apps => web}/admin/src/components/UsersPanel.tsx (100%) rename {apps => web}/admin/src/components/ui/badge.tsx (100%) rename {apps => web}/admin/src/components/ui/button.tsx (100%) rename {apps => web}/admin/src/components/ui/card.tsx (100%) rename {apps => web}/admin/src/components/ui/input.tsx (100%) rename {apps => web}/admin/src/index.css (100%) rename {apps => web}/admin/src/lib/api.ts (100%) rename {apps => web}/admin/src/lib/auth.ts (100%) rename {apps => web}/admin/src/lib/utils.ts (100%) rename {apps => web}/admin/src/main.tsx (100%) rename {apps => web}/admin/tsconfig.json (100%) rename {apps => web}/admin/vite.config.ts (100%) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ef550b3..01dd4dc 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -22,8 +22,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version-file: services/api/go.mod - cache-dependency-path: services/api/go.sum + go-version-file: go.mod + cache-dependency-path: go.sum - uses: actions/setup-node@v4 with: node-version: 22 @@ -31,5 +31,5 @@ jobs: - run: npm ci - run: make build - run: make test - - run: go vet ./services/api/... - - run: test -z "$(gofmt -l services/api)" + - run: go vet ./... + - run: test -z "$(gofmt -l cmd internal)" diff --git a/.gitignore b/.gitignore index 4439061..15d1a68 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,6 @@ bin/ .idea/ *.log coverage/ -!services/api/internal/platform/adminui/dist/ -services/api/internal/platform/adminui/dist/* -!services/api/internal/platform/adminui/dist/.keep +!internal/adminui/dist/ +internal/adminui/dist/* +!internal/adminui/dist/.keep diff --git a/AGENTS.md b/AGENTS.md index 64911c2..4fb9eba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ 项目背景、已完成功能和验证状态见 `docs/development-state.md`。继续开发前阅读该文件,完成工作后更新事实与待办,不记录密码。 - 后端使用 Go,遵循 gofmt、go vet、惯用错误处理、context 传递、明确依赖注入及 cmd/internal 分层。 +- 后端采用仓库根目录的单 Go module:cmd 放入口,internal 按 model、service、repository、handler、config、database 等职责组织;管理端位于 web/admin。避免为单体项目重复嵌套 services/api、业务域目录和 platform 层,不引入单模块 go.work。 - HTTP handler 只做协议适配,业务规则归 service/domain,SQL 归 repository。不得将 Gin Context 传入业务层。 - PostgreSQL 所有表(包括迁移、任务和关联表)必须以 `veyra_` 开头,表与字段使用 snake_case。 - 主键 `id` 使用字符串 UUID:Go 类型 `string`,数据库 `varchar(36)` 并校验 UUID 格式。外键保持相同类型。 diff --git a/Makefile b/Makefile index 5b71c39..d53afa1 100644 --- a/Makefile +++ b/Makefile @@ -2,24 +2,24 @@ export GOCACHE := $(CURDIR)/.cache/go-build api: admin-build - go run ./services/api/cmd/api -env-file .env + go run ./cmd/api -env-file .env build: admin-build mkdir -p bin - go build -trimpath -o bin/veyra-api ./services/api/cmd/api - go build -trimpath -o bin/veyra-db ./services/api/cmd/db - go build -trimpath -o bin/veyra-user ./services/api/cmd/user + go build -trimpath -o bin/veyra-api ./cmd/api + go build -trimpath -o bin/veyra-db ./cmd/db + go build -trimpath -o bin/veyra-user ./cmd/user db-create: - go run ./services/api/cmd/db -env-file .env -action create + go run ./cmd/db -env-file .env -action create db-migrate: - go run ./services/api/cmd/db -env-file .env -action migrate + go run ./cmd/db -env-file .env -action migrate admin-create: - go run ./services/api/cmd/user -env-file .env -role ADMIN + go run ./cmd/user -env-file .env -role ADMIN user-create: - go run ./services/api/cmd/user -env-file .env -role USER + go run ./cmd/user -env-file .env -role USER test: - go test -race ./services/api/... + go test -race ./... check: - go vet ./services/api/... + go vet ./... npm run build --workspace @veyra/admin admin-dev: npm run dev --workspace @veyra/admin diff --git a/README.md b/README.md index b86c00b..1904d69 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ cp .env.example .env 将 PostgreSQL 用户名与密码填写到 `.env`。默认数据库名称 `veyra`,默认管理数据库 `postgres`;创建数据库需要连接角色具有 CREATEDB 权限。Redis 无用户名,有密码:在本地 `.env` 配置 `REDIS_PASSWORD`,保持 `REDIS_USERNAME` 为空。`PGSSLMODE=disable` 是本地模板值,如服务器要求 TLS 则修改。 ```sh -go mod download -C services/api +go mod download npm ci make db-create make db-migrate @@ -71,18 +71,28 @@ VEYRA_TEST_DATABASE_URL='postgres://user:password@host:5432/test_db?sslmode=disa ## 目录 ```text -apps/admin/ Vite React 管理端 -services/api/cmd/api/ API 入口 -services/api/cmd/db/ 建库与迁移入口 -services/api/cmd/user/ 开发身份初始化入口 -services/api/internal/identity/ 身份领域 -services/api/internal/portfolio/ 账户领域 -services/api/internal/operations/ 服务健康业务 -services/api/internal/platform/ 配置与数据库设施 -services/api/internal/transport/ HTTP 协议适配 -services/api/api/openapi.yaml 当前 HTTP 契约 +cmd/ + api/ API 入口与依赖装配 + db/ 建库与迁移命令 + user/ 开发身份初始化命令 +internal/ + config/ 配置加载与校验 + database/migrations/ 数据库连接与版本化迁移 + model/ User、Account、Session 等模型 + repository/ PostgreSQL 数据访问与事务 + service/ 登录、账户和健康检查业务 + handler/ HTTP 路由、中间件与协议适配 + adminui/ 管理端 embed 资源与静态服务 + apperror/ 跨层业务错误 + ratelimit/ Redis 登录限流 + integration/ 数据库集成测试 +web/admin/ Vite React 管理端 +api/openapi.yaml HTTP 契约 +go.mod / go.sum 根目录唯一 Go module ``` +仓库采用根目录 Go 单模块结构,管理端放在 `web/admin`,无需 `go.work`。文件按职责划分,调用方向为 handler → service → repository;接口在 service 中定义,由 repository 实现,避免 service 依赖具体数据库。 + 采用显式 SQL 和结构体 db tag,避免 ORM 隐式推断表名。迁移打包到 Go 二进制,已执行迁移不可改写,后续修改新增版本。API 启动不自动建库或迁移。健康检查不会返回连接串和供应商错误细节。 ## 待完成 diff --git a/services/api/api/openapi.yaml b/api/openapi.yaml similarity index 100% rename from services/api/api/openapi.yaml rename to api/openapi.yaml diff --git a/services/api/cmd/api/main.go b/cmd/api/main.go similarity index 60% rename from services/api/cmd/api/main.go rename to cmd/api/main.go index 01b6ade..3cc8e75 100644 --- a/services/api/cmd/api/main.go +++ b/cmd/api/main.go @@ -13,14 +13,12 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/redis/go-redis/v9" - identityrepo "veyra/services/api/internal/identity/repository" - identityservice "veyra/services/api/internal/identity/service" - "veyra/services/api/internal/operations/service" - "veyra/services/api/internal/platform/config" - "veyra/services/api/internal/platform/ratelimit" - portfoliorepo "veyra/services/api/internal/portfolio/repository" - portfolioservice "veyra/services/api/internal/portfolio/service" - transport "veyra/services/api/internal/transport/http" + + "git.sundynix.cn/Blizzard/Veyra/internal/config" + "git.sundynix.cn/Blizzard/Veyra/internal/handler" + "git.sundynix.cn/Blizzard/Veyra/internal/ratelimit" + "git.sundynix.cn/Blizzard/Veyra/internal/repository" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func main() { @@ -44,8 +42,8 @@ func run(logger *slog.Logger) error { return err } var postgres service.Probe - var authRepo identityservice.Repository - var accountRepo portfolioservice.Repository + var authRepo service.AuthRepository + var accountRepo service.AccountRepository if c.PGUser != "" { if err := c.ValidateDatabase(); err != nil { return err @@ -56,18 +54,18 @@ func run(logger *slog.Logger) error { } defer pool.Close() postgres = pool.Ping - authRepo = identityrepo.NewPostgres(pool) - accountRepo = portfoliorepo.NewPostgres(pool) + authRepo = repository.NewIdentityRepository(pool) + accountRepo = repository.NewAccountRepository(pool) } redisClient := redis.NewClient(&redis.Options{Addr: c.RedisAddr, Username: c.RedisUsername, Password: c.RedisPassword, DB: c.RedisDB, DialTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, MaxRetries: -1, ContextTimeoutEnabled: true}) defer redisClient.Close() health := service.NewHealthService(postgres, func(ctx context.Context) error { return redisClient.Ping(ctx).Err() }) - auth, err := identityservice.NewAuthService(authRepo) + auth, err := service.NewAuthService(authRepo) if err != nil { return err } - identityOptions := transport.IdentityOptions{Auth: auth, Accounts: portfolioservice.NewAccountService(accountRepo), Limiter: ratelimit.NewRedis(redisClient), DevelopmentLogin: c.Environment == "local" || c.Environment == "test", SecureCookie: c.Environment != "local" && c.Environment != "test"} - server := &http.Server{Addr: c.HTTPAddr, Handler: transport.NewRouter(health, logger, identityOptions), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20} + identityOptions := handler.IdentityOptions{Auth: auth, Accounts: service.NewAccountService(accountRepo), Limiter: ratelimit.NewRedis(redisClient), DevelopmentLogin: c.Environment == "local" || c.Environment == "test", SecureCookie: c.Environment != "local" && c.Environment != "test"} + server := &http.Server{Addr: c.HTTPAddr, Handler: handler.NewRouter(health, logger, identityOptions), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20} ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() result := make(chan error, 1) diff --git a/services/api/cmd/db/main.go b/cmd/db/main.go similarity index 91% rename from services/api/cmd/db/main.go rename to cmd/db/main.go index b8144f1..527ca26 100644 --- a/services/api/cmd/db/main.go +++ b/cmd/db/main.go @@ -8,8 +8,8 @@ import ( "os" "time" - "veyra/services/api/internal/platform/config" - "veyra/services/api/internal/platform/database" + "git.sundynix.cn/Blizzard/Veyra/internal/config" + "git.sundynix.cn/Blizzard/Veyra/internal/database" ) func main() { diff --git a/services/api/cmd/user/main.go b/cmd/user/main.go similarity index 85% rename from services/api/cmd/user/main.go rename to cmd/user/main.go index fa92716..0886372 100644 --- a/services/api/cmd/user/main.go +++ b/cmd/user/main.go @@ -9,10 +9,10 @@ import ( "os" "time" - "veyra/services/api/internal/identity/repository" - "veyra/services/api/internal/identity/service" - "veyra/services/api/internal/platform/config" - "veyra/services/api/internal/platform/database" + "git.sundynix.cn/Blizzard/Veyra/internal/config" + "git.sundynix.cn/Blizzard/Veyra/internal/database" + "git.sundynix.cn/Blizzard/Veyra/internal/repository" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func main() { @@ -60,7 +60,7 @@ func run() error { return err } defer pool.Close() - auth, err := service.NewAuthService(repository.NewPostgres(pool)) + auth, err := service.NewAuthService(repository.NewIdentityRepository(pool)) if err != nil { return err } diff --git a/docs/development-state.md b/docs/development-state.md index 70bfb29..e9655dd 100644 --- a/docs/development-state.md +++ b/docs/development-state.md @@ -33,6 +33,19 @@ - OpenAPI、依赖锁文件、Makefile 和带 PostgreSQL 集成测试服务的 CI。 - `make build` 生成内嵌管理端的 veyra-api,以及 veyra-db 和 veyra-user。 +## 当前工程目录 + +- 用户指出旧后端目录过深,当前已改为仓库根目录的单 Go module。 +- module 为 git.sundynix.cn/Blizzard/Veyra,与仓库地址一致;删除单模块 go.work。 +- cmd/api、cmd/db、cmd/user 放入口与装配。 +- internal/model 放模型,internal/service 放业务,internal/repository 放 SQL 与事务,internal/handler 放 HTTP 路由和协议适配。 +- internal/config、database、adminui、apperror、ratelimit 放当前实际需要的设施;避免多余嵌套和空目录。 +- 管理端由 apps/admin 迁至 web/admin;embed 构建产物由 internal/adminui/dist 提供。 +- Makefile、CI、npm workspace/锁文件、Go 导入路径与 README 一并更新,原有命令和接口保持一致。 +- 原始设计报告中的 monorepo 目录仅为旧规划,当前实现以本文件、AGENTS.md 和 README 的结构为准。 +- 迁移文件仅移动路径,已逐字节核对两版 SQL 与原提交一致,不影响已执行迁移的 checksum。 +- 新结构已通过前端构建与 embed、Go 编译、race 检查和 go vet;真实数据库集成测试仍因连接限制跳过,不将目录迁移视作数据库验证。 + ## 验证与环境事实 - 已通过前端 TypeScript 检查与生产构建、Go 编译、go vet、单元与无监听 HTTP 测试(含 race 检查)。 @@ -58,3 +71,9 @@ 用户本轮明确要求保存记忆、git add、commit 和 push。目标为当前 master 分支的 origin;操作结果以实际 Git 输出为准,禁止将失败的提交或推送记作已完成。 本轮操作结果:项目记忆与忽略规则已保存,候选文件检查未发现本地配置的长密码。仓库当前没有提交;git add 因无法创建 .git/index.lock(Operation not permitted)失败,尚未 commit/push。远程分支读取失败,原因是执行环境无法解析远程域名。后续需在有 Git 写权限和网络访问能力的终端完成提交与推送,先确认远程状态;不强制推送、不覆盖远程历史。 + +后续检查发现本地已有 ba6dfff(init: initial commit)等提交,说明前述无提交状态已变化;远程推送状态未经核验。此次目录调整尚未提交,Git 输出中的旧路径删除与新路径新增是迁移表现,提交时应一并暂存。 + +## 本轮开发状态 + +原计划推进模拟候选修订、审核与发布;用户中途指出目录不合理,本轮优先完成工程结构纠正与回归。候选功能尚未接入仓库交付代码,不新增第三版迁移;相关草稿保留在本地 .cache/candidate-draft(不提交),后续必须按新结构重新审查、实现和测试。下一步仍为候选审核闭环,不能将其描述为已完成。 diff --git a/services/api/go.mod b/go.mod similarity index 98% rename from services/api/go.mod rename to go.mod index 3c261dc..5d4f989 100644 --- a/services/api/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module veyra/services/api +module git.sundynix.cn/Blizzard/Veyra go 1.26.2 diff --git a/services/api/go.sum b/go.sum similarity index 100% rename from services/api/go.sum rename to go.sum diff --git a/go.work b/go.work deleted file mode 100644 index 272037e..0000000 --- a/go.work +++ /dev/null @@ -1,3 +0,0 @@ -go 1.26.2 - -use ./services/api diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 113a675..0000000 --- a/go.work.sum +++ /dev/null @@ -1,32 +0,0 @@ -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= -github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc= -github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= -github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/services/api/internal/platform/adminui/dist/.keep b/internal/adminui/dist/.keep similarity index 100% rename from services/api/internal/platform/adminui/dist/.keep rename to internal/adminui/dist/.keep diff --git a/services/api/internal/platform/adminui/handler.go b/internal/adminui/handler.go similarity index 100% rename from services/api/internal/platform/adminui/handler.go rename to internal/adminui/handler.go diff --git a/services/api/internal/platform/apperror/errors.go b/internal/apperror/errors.go similarity index 100% rename from services/api/internal/platform/apperror/errors.go rename to internal/apperror/errors.go diff --git a/services/api/internal/platform/config/config.go b/internal/config/config.go similarity index 100% rename from services/api/internal/platform/config/config.go rename to internal/config/config.go diff --git a/services/api/internal/platform/config/config_test.go b/internal/config/config_test.go similarity index 100% rename from services/api/internal/platform/config/config_test.go rename to internal/config/config_test.go diff --git a/services/api/internal/platform/database/database.go b/internal/database/database.go similarity index 98% rename from services/api/internal/platform/database/database.go rename to internal/database/database.go index 55bf81e..b3e00af 100644 --- a/services/api/internal/platform/database/database.go +++ b/internal/database/database.go @@ -10,7 +10,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" - "veyra/services/api/internal/platform/config" + + "git.sundynix.cn/Blizzard/Veyra/internal/config" ) //go:embed migrations/*.sql diff --git a/services/api/internal/platform/database/database_test.go b/internal/database/database_test.go similarity index 100% rename from services/api/internal/platform/database/database_test.go rename to internal/database/database_test.go diff --git a/services/api/internal/platform/database/migrations/000001_foundation.sql b/internal/database/migrations/000001_foundation.sql similarity index 100% rename from services/api/internal/platform/database/migrations/000001_foundation.sql rename to internal/database/migrations/000001_foundation.sql diff --git a/services/api/internal/platform/database/migrations/000002_identity_account.sql b/internal/database/migrations/000002_identity_account.sql similarity index 100% rename from services/api/internal/platform/database/migrations/000002_identity_account.sql rename to internal/database/migrations/000002_identity_account.sql diff --git a/services/api/internal/transport/http/identity.go b/internal/handler/identity.go similarity index 95% rename from services/api/internal/transport/http/identity.go rename to internal/handler/identity.go index 3317176..7fb8f97 100644 --- a/services/api/internal/transport/http/identity.go +++ b/internal/handler/identity.go @@ -1,4 +1,4 @@ -package http +package handler import ( "context" @@ -9,20 +9,20 @@ import ( "net/url" "strings" "time" - "veyra/services/api/internal/platform/apperror" "github.com/gin-gonic/gin" - "veyra/services/api/internal/identity/domain" - identity "veyra/services/api/internal/identity/service" - portfolio "veyra/services/api/internal/portfolio/service" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) type LoginLimiter interface { Login(context.Context, string, string) (bool, error) } type IdentityOptions struct { - Auth *identity.AuthService - Accounts *portfolio.AccountService + Auth *service.AuthService + Accounts *service.AccountService Limiter LoginLimiter DevelopmentLogin bool SecureCookie bool @@ -193,7 +193,7 @@ func requestToken(r *http.Request, audience string) string { return token } -func currentUser(c *gin.Context) domain.User { return c.MustGet("user").(domain.User) } +func currentUser(c *gin.Context) model.User { return c.MustGet("user").(model.User) } // The embedded console and Vite development proxy both call APIs on their own origin. func sameOrigin(r *http.Request) bool { diff --git a/services/api/internal/transport/http/identity_test.go b/internal/handler/identity_test.go similarity index 77% rename from services/api/internal/transport/http/identity_test.go rename to internal/handler/identity_test.go index fcc11ce..d291f32 100644 --- a/services/api/internal/transport/http/identity_test.go +++ b/internal/handler/identity_test.go @@ -1,4 +1,4 @@ -package http +package handler import ( "context" @@ -9,17 +9,16 @@ import ( "strings" "testing" - identity "veyra/services/api/internal/identity/service" - operations "veyra/services/api/internal/operations/service" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func TestAuthenticationAndCSRFBoundaries(t *testing.T) { - auth, err := identity.NewAuthService(nil) + auth, err := service.NewAuthService(nil) if err != nil { t.Fatal(err) } probe := func(context.Context) error { return nil } - router := NewRouter(operations.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{Auth: auth, DevelopmentLogin: true}) + router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{Auth: auth, DevelopmentLogin: true}) for _, tc := range []struct { method, path, body, origin string status int @@ -44,7 +43,7 @@ func TestAuthenticationAndCSRFBoundaries(t *testing.T) { func TestDevelopmentLoginDisabledByDefault(t *testing.T) { probe := func(context.Context) error { return nil } - router := NewRouter(operations.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{}) + router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{}) rec := httptest.NewRecorder() router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(`{}`))) if rec.Code != http.StatusServiceUnavailable { diff --git a/services/api/internal/transport/http/router.go b/internal/handler/router.go similarity index 90% rename from services/api/internal/transport/http/router.go rename to internal/handler/router.go index 8f09e9a..643995e 100644 --- a/services/api/internal/transport/http/router.go +++ b/internal/handler/router.go @@ -1,5 +1,5 @@ -// Package http adapts application services to the HTTP protocol. -package http +// Package handler adapts application services to the HTTP protocol. +package handler import ( "context" @@ -10,8 +10,9 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - "veyra/services/api/internal/operations/service" - "veyra/services/api/internal/platform/adminui" + + "git.sundynix.cn/Blizzard/Veyra/internal/adminui" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func NewRouter(health *service.HealthService, logger *slog.Logger, identity IdentityOptions) http.Handler { diff --git a/services/api/internal/transport/http/router_test.go b/internal/handler/router_test.go similarity index 97% rename from services/api/internal/transport/http/router_test.go rename to internal/handler/router_test.go index 7d8d567..52f0ae6 100644 --- a/services/api/internal/transport/http/router_test.go +++ b/internal/handler/router_test.go @@ -1,4 +1,4 @@ -package http +package handler import ( "context" @@ -10,7 +10,7 @@ import ( "strings" "testing" - "veyra/services/api/internal/operations/service" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func TestLivenessAndReadinessAreIndependent(t *testing.T) { diff --git a/services/api/internal/integration/identity_account_test.go b/internal/integration/identity_account_test.go similarity index 87% rename from services/api/internal/integration/identity_account_test.go rename to internal/integration/identity_account_test.go index f8e9eab..95dc75a 100644 --- a/services/api/internal/integration/identity_account_test.go +++ b/internal/integration/identity_account_test.go @@ -14,14 +14,12 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" - identityrepo "veyra/services/api/internal/identity/repository" - identity "veyra/services/api/internal/identity/service" - operations "veyra/services/api/internal/operations/service" - "veyra/services/api/internal/platform/apperror" - "veyra/services/api/internal/platform/database" - portfoliorepo "veyra/services/api/internal/portfolio/repository" - portfolio "veyra/services/api/internal/portfolio/service" - transport "veyra/services/api/internal/transport/http" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/database" + "git.sundynix.cn/Blizzard/Veyra/internal/handler" + "git.sundynix.cn/Blizzard/Veyra/internal/repository" + "git.sundynix.cn/Blizzard/Veyra/internal/service" ) func TestIdentityAccountLifecycle(t *testing.T) { @@ -58,7 +56,7 @@ func TestIdentityAccountLifecycle(t *testing.T) { if err := database.Migrate(ctx, pool); err != nil { t.Fatal(err) } - auth, err := identity.NewAuthService(identityrepo.NewPostgres(pool)) + auth, err := service.NewAuthService(repository.NewIdentityRepository(pool)) if err != nil { t.Fatal(err) } @@ -93,7 +91,7 @@ func TestIdentityAccountLifecycle(t *testing.T) { if _, err := auth.Authenticate(ctx, login.Token, "app"); !errors.Is(err, apperror.ErrUnauthenticated) { t.Fatal("revoked session remained valid") } - accounts := portfolio.NewAccountService(portfoliorepo.NewPostgres(pool)) + accounts := service.NewAccountService(repository.NewAccountRepository(pool)) cash := "100000.01" a, err := accounts.Create(ctx, u1.ID, "simulation", "Simulation", &cash) if err != nil || a.CashBalance == nil || *a.CashBalance != cash { @@ -123,7 +121,7 @@ func TestIdentityAccountLifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := pool.Exec(ctx, "UPDATE veyra_session SET created_at=now()-interval '1 day',expires_at=now()-interval '1 hour' WHERE token_hash=$1", identity.TokenHash(login.Token)); err != nil { + if _, err := pool.Exec(ctx, "UPDATE veyra_session SET created_at=now()-interval '1 day',expires_at=now()-interval '1 hour' WHERE token_hash=$1", service.TokenHash(login.Token)); err != nil { t.Fatal(err) } if _, err := auth.Authenticate(ctx, login.Token, "app"); !errors.Is(err, apperror.ErrUnauthenticated) { @@ -133,7 +131,7 @@ func TestIdentityAccountLifecycle(t *testing.T) { t.Fatal(err) } probe := func(context.Context) error { return nil } - router := transport.NewRouter(operations.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), transport.IdentityOptions{Auth: auth, Accounts: accounts, Limiter: allowLogin{}, DevelopmentLogin: true}) + router := handler.NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), handler.IdentityOptions{Auth: auth, Accounts: accounts, Limiter: allowLogin{}, DevelopmentLogin: true}) call := func(method, path, body, origin string, cookie *http.Cookie) *httptest.ResponseRecorder { req := httptest.NewRequest(method, "http://example.com"+path, strings.NewReader(body)) if body != "" { diff --git a/services/api/internal/portfolio/domain/account.go b/internal/model/account.go similarity index 96% rename from services/api/internal/portfolio/domain/account.go rename to internal/model/account.go index ff74346..93850ee 100644 --- a/services/api/internal/portfolio/domain/account.go +++ b/internal/model/account.go @@ -1,4 +1,4 @@ -package domain +package model import "time" diff --git a/services/api/internal/identity/domain/auth.go b/internal/model/auth.go similarity index 97% rename from services/api/internal/identity/domain/auth.go rename to internal/model/auth.go index bbf0615..c088821 100644 --- a/services/api/internal/identity/domain/auth.go +++ b/internal/model/auth.go @@ -1,4 +1,4 @@ -package domain +package model import ( "time" diff --git a/services/api/internal/portfolio/domain/cash_entry.go b/internal/model/cash_entry.go similarity index 96% rename from services/api/internal/portfolio/domain/cash_entry.go rename to internal/model/cash_entry.go index 5bcab04..f7b8000 100644 --- a/services/api/internal/portfolio/domain/cash_entry.go +++ b/internal/model/cash_entry.go @@ -1,4 +1,4 @@ -package domain +package model import "time" diff --git a/services/api/internal/portfolio/domain/money.go b/internal/model/money.go similarity index 92% rename from services/api/internal/portfolio/domain/money.go rename to internal/model/money.go index be77705..cbb3cd2 100644 --- a/services/api/internal/portfolio/domain/money.go +++ b/internal/model/money.go @@ -1,11 +1,12 @@ -package domain +package model import ( "fmt" "regexp" "strconv" "strings" - "veyra/services/api/internal/platform/apperror" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" ) var moneyPattern = regexp.MustCompile(`^(0|[1-9][0-9]{0,9})(\.[0-9]{1,2})?$`) diff --git a/services/api/internal/portfolio/domain/money_test.go b/internal/model/money_test.go similarity index 97% rename from services/api/internal/portfolio/domain/money_test.go rename to internal/model/money_test.go index 637cdf2..c0e438d 100644 --- a/services/api/internal/portfolio/domain/money_test.go +++ b/internal/model/money_test.go @@ -1,4 +1,4 @@ -package domain +package model import "testing" diff --git a/services/api/internal/platform/database/schema_migration.go b/internal/model/schema_migration.go similarity index 94% rename from services/api/internal/platform/database/schema_migration.go rename to internal/model/schema_migration.go index b736d1d..506b460 100644 --- a/services/api/internal/platform/database/schema_migration.go +++ b/internal/model/schema_migration.go @@ -1,4 +1,4 @@ -package database +package model import "time" diff --git a/services/api/internal/identity/domain/user.go b/internal/model/user.go similarity index 96% rename from services/api/internal/identity/domain/user.go rename to internal/model/user.go index 7289636..0c03367 100644 --- a/services/api/internal/identity/domain/user.go +++ b/internal/model/user.go @@ -1,4 +1,4 @@ -package domain +package model import "time" diff --git a/services/api/internal/platform/ratelimit/redis.go b/internal/ratelimit/redis.go similarity index 100% rename from services/api/internal/platform/ratelimit/redis.go rename to internal/ratelimit/redis.go diff --git a/services/api/internal/portfolio/repository/postgres.go b/internal/repository/account.go similarity index 57% rename from services/api/internal/portfolio/repository/postgres.go rename to internal/repository/account.go index aa9cdbd..ee09687 100644 --- a/services/api/internal/portfolio/repository/postgres.go +++ b/internal/repository/account.go @@ -2,78 +2,79 @@ package repository import ( "context" - "errors" - "veyra/services/api/internal/platform/apperror" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - "veyra/services/api/internal/portfolio/domain" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" ) -type Postgres struct{ pool *pgxpool.Pool } +type AccountRepository struct{ pool *pgxpool.Pool } -func NewPostgres(pool *pgxpool.Pool) *Postgres { return &Postgres{pool: pool} } +func NewAccountRepository(pool *pgxpool.Pool) *AccountRepository { + return &AccountRepository{pool: pool} +} -func (r *Postgres) Create(ctx context.Context, a domain.Account, entry *domain.CashEntry) (domain.AccountSummary, error) { +func (r *AccountRepository) Create(ctx context.Context, a model.Account, entry *model.CashEntry) (model.AccountSummary, error) { tx, err := r.pool.Begin(ctx) if err != nil { - return domain.AccountSummary{}, apperror.ErrUnavailable + return model.AccountSummary{}, apperror.ErrUnavailable } defer tx.Rollback(context.Background()) // Lock the owner so account creation cannot race with disabling the user. var status string if err := tx.QueryRow(ctx, `SELECT status FROM veyra_user WHERE id=$1 AND role='USER' FOR UPDATE`, a.UserID).Scan(&status); err != nil { - return domain.AccountSummary{}, mapError(err) + return model.AccountSummary{}, mapError(err) } if status != "ACTIVE" { - return domain.AccountSummary{}, apperror.ErrForbidden + return model.AccountSummary{}, apperror.ErrForbidden } err = tx.QueryRow(ctx, `INSERT INTO veyra_account(id,user_id,mode,name,currency,ledger_status) VALUES($1,$2,$3,$4,$5,$6) RETURNING created_at`, a.ID, a.UserID, a.Mode, a.Name, a.Currency, a.LedgerStatus).Scan(&a.CreatedAt) if err != nil { - return domain.AccountSummary{}, mapError(err) + return model.AccountSummary{}, mapError(err) } var balance *string if entry != nil { if _, err := tx.Exec(ctx, `INSERT INTO veyra_cash_entry(id,account_id,entry_type,amount_minor) VALUES($1,$2,$3,$4)`, entry.ID, entry.AccountID, entry.EntryType, entry.AmountMinor); err != nil { - return domain.AccountSummary{}, mapError(err) + return model.AccountSummary{}, mapError(err) } - value := domain.FormatMoney(entry.AmountMinor) + value := model.FormatMoney(entry.AmountMinor) balance = &value } if err := tx.Commit(ctx); err != nil { - return domain.AccountSummary{}, apperror.ErrUnavailable + return model.AccountSummary{}, apperror.ErrUnavailable } - return domain.AccountSummary{Account: a, CashBalance: balance}, nil + return model.AccountSummary{Account: a, CashBalance: balance}, nil } const summarySelect = `SELECT a.id,a.user_id,a.mode,a.name,a.currency,a.ledger_status,a.created_at,CASE WHEN a.ledger_status='COMPLETE' THEN (SELECT COALESCE(sum(e.amount_minor),0)::bigint FROM veyra_cash_entry e WHERE e.account_id=a.id) ELSE NULL END FROM veyra_account a` -func scanSummary(row pgx.Row) (domain.AccountSummary, error) { - var s domain.AccountSummary +func scanSummary(row pgx.Row) (model.AccountSummary, error) { + var s model.AccountSummary var minor *int64 err := row.Scan(&s.ID, &s.UserID, &s.Mode, &s.Name, &s.Currency, &s.LedgerStatus, &s.CreatedAt, &minor) if err != nil { return s, mapError(err) } if minor != nil { - value := domain.FormatMoney(*minor) + value := model.FormatMoney(*minor) s.CashBalance = &value } return s, nil } -func (r *Postgres) Get(ctx context.Context, userID, accountID string) (domain.AccountSummary, error) { +func (r *AccountRepository) Get(ctx context.Context, userID, accountID string) (model.AccountSummary, error) { return scanSummary(r.pool.QueryRow(ctx, summarySelect+` WHERE a.user_id=$1 AND a.id=$2`, userID, accountID)) } -func (r *Postgres) List(ctx context.Context, userID string) ([]domain.AccountSummary, error) { +func (r *AccountRepository) List(ctx context.Context, userID string) ([]model.AccountSummary, error) { rows, err := r.pool.Query(ctx, summarySelect+` WHERE a.user_id=$1 ORDER BY a.created_at,a.id`, userID) if err != nil { return nil, apperror.ErrUnavailable } defer rows.Close() - list := make([]domain.AccountSummary, 0) + list := make([]model.AccountSummary, 0) for rows.Next() { s, err := scanSummary(rows) if err != nil { @@ -83,17 +84,3 @@ func (r *Postgres) List(ctx context.Context, userID string) ([]domain.AccountSum } return list, mapError(rows.Err()) } - -func mapError(err error) error { - if err == nil { - return nil - } - if errors.Is(err, pgx.ErrNoRows) { - return apperror.ErrNotFound - } - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == "23505" { - return apperror.ErrConflict - } - return apperror.ErrUnavailable -} diff --git a/internal/repository/errors.go b/internal/repository/errors.go new file mode 100644 index 0000000..58801b0 --- /dev/null +++ b/internal/repository/errors.go @@ -0,0 +1,24 @@ +package repository + +import ( + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" +) + +func mapError(err error) error { + if err == nil { + return nil + } + if errors.Is(err, pgx.ErrNoRows) { + return apperror.ErrNotFound + } + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return apperror.ErrConflict + } + return apperror.ErrUnavailable +} diff --git a/services/api/internal/identity/repository/postgres.go b/internal/repository/identity.go similarity index 62% rename from services/api/internal/identity/repository/postgres.go rename to internal/repository/identity.go index c9eec54..3735a2e 100644 --- a/services/api/internal/identity/repository/postgres.go +++ b/internal/repository/identity.go @@ -4,73 +4,75 @@ import ( "context" "errors" "time" - "veyra/services/api/internal/platform/apperror" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - "veyra/services/api/internal/identity/domain" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" ) -type Postgres struct{ pool *pgxpool.Pool } +type IdentityRepository struct{ pool *pgxpool.Pool } -func NewPostgres(pool *pgxpool.Pool) *Postgres { return &Postgres{pool: pool} } +func NewIdentityRepository(pool *pgxpool.Pool) *IdentityRepository { + return &IdentityRepository{pool: pool} +} -func (r *Postgres) CreateUser(ctx context.Context, user domain.User, credential domain.Credential) (domain.User, error) { +func (r *IdentityRepository) CreateUser(ctx context.Context, user model.User, credential model.Credential) (model.User, error) { tx, err := r.pool.Begin(ctx) if err != nil { - return domain.User{}, apperror.ErrUnavailable + return model.User{}, apperror.ErrUnavailable } defer tx.Rollback(context.Background()) err = tx.QueryRow(ctx, `INSERT INTO veyra_user(id,display_name,status,role) VALUES($1,$2,$3,$4) RETURNING created_at,updated_at`, user.ID, user.DisplayName, user.Status, user.Role).Scan(&user.CreatedAt, &user.UpdatedAt) if err != nil { - return domain.User{}, mapError(err) + return model.User{}, mapError(err) } _, err = tx.Exec(ctx, `INSERT INTO veyra_credential(id,user_id,login_name,password_hash) VALUES($1,$2,$3,$4)`, credential.ID, credential.UserID, credential.LoginName, credential.PasswordHash) if err != nil { - return domain.User{}, mapError(err) + return model.User{}, mapError(err) } if err = tx.Commit(ctx); err != nil { - return domain.User{}, apperror.ErrUnavailable + return model.User{}, apperror.ErrUnavailable } return user, nil } -func (r *Postgres) FindCredential(ctx context.Context, login string) (domain.User, domain.Credential, error) { - var u domain.User - var c domain.Credential +func (r *IdentityRepository) FindCredential(ctx context.Context, login string) (model.User, model.Credential, error) { + var u model.User + var c model.Credential err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at,c.id,c.user_id,c.login_name,c.password_hash,c.created_at FROM veyra_user u JOIN veyra_credential c ON c.user_id=u.id WHERE c.login_name=$1`, login).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt, &c.ID, &c.UserID, &c.LoginName, &c.PasswordHash, &c.CreatedAt) return u, c, mapError(err) } -func (r *Postgres) CreateSession(ctx context.Context, s domain.Session) error { +func (r *IdentityRepository) CreateSession(ctx context.Context, s model.Session) error { _, err := r.pool.Exec(ctx, `INSERT INTO veyra_session(id,user_id,token_hash,audience,expires_at) VALUES($1,$2,$3,$4,$5)`, s.ID, s.UserID, s.TokenHash, s.Audience, s.ExpiresAt) return mapError(err) } -func (r *Postgres) FindSessionUser(ctx context.Context, hash, audience string, now time.Time) (domain.User, error) { - var u domain.User +func (r *IdentityRepository) FindSessionUser(ctx context.Context, hash, audience string, now time.Time) (model.User, error) { + var u model.User err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at FROM veyra_session s JOIN veyra_user u ON u.id=s.user_id WHERE s.token_hash=$1 AND s.audience=$2 AND s.expires_at>$3 AND s.revoked_at IS NULL AND u.status='ACTIVE' AND (($2='admin' AND u.role='ADMIN') OR ($2='app' AND u.role='USER'))`, hash, audience, now).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { - return domain.User{}, apperror.ErrUnauthenticated + return model.User{}, apperror.ErrUnauthenticated } return u, mapError(err) } -func (r *Postgres) RevokeSession(ctx context.Context, hash, audience string, now time.Time) error { +func (r *IdentityRepository) RevokeSession(ctx context.Context, hash, audience string, now time.Time) error { _, err := r.pool.Exec(ctx, `UPDATE veyra_session SET revoked_at=$3 WHERE token_hash=$1 AND audience=$2 AND revoked_at IS NULL`, hash, audience, now) return mapError(err) } -func (r *Postgres) ListUsers(ctx context.Context, limit int) ([]domain.User, error) { +func (r *IdentityRepository) ListUsers(ctx context.Context, limit int) ([]model.User, error) { rows, err := r.pool.Query(ctx, `SELECT id,display_name,status,role,created_at,updated_at FROM veyra_user WHERE role='USER' ORDER BY created_at DESC,id DESC LIMIT $1`, limit) if err != nil { return nil, apperror.ErrUnavailable } defer rows.Close() - users := make([]domain.User, 0) + users := make([]model.User, 0) for rows.Next() { - var u domain.User + var u model.User if err := rows.Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil { return nil, apperror.ErrUnavailable } @@ -78,17 +80,3 @@ func (r *Postgres) ListUsers(ctx context.Context, limit int) ([]domain.User, err } return users, mapError(rows.Err()) } - -func mapError(err error) error { - if err == nil { - return nil - } - if errors.Is(err, pgx.ErrNoRows) { - return apperror.ErrNotFound - } - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == "23505" { - return apperror.ErrConflict - } - return apperror.ErrUnavailable -} diff --git a/internal/service/account.go b/internal/service/account.go new file mode 100644 index 0000000..3c43c3d --- /dev/null +++ b/internal/service/account.go @@ -0,0 +1,70 @@ +package service + +import ( + "context" + "strings" + "unicode/utf8" + + "github.com/google/uuid" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" +) + +type AccountRepository interface { + Create(context.Context, model.Account, *model.CashEntry) (model.AccountSummary, error) + List(context.Context, string) ([]model.AccountSummary, error) + Get(context.Context, string, string) (model.AccountSummary, error) +} + +type AccountService struct{ repo AccountRepository } + +func NewAccountService(repo AccountRepository) *AccountService { return &AccountService{repo: repo} } + +func (s *AccountService) Create(ctx context.Context, userID, mode, name string, initialCash *string) (model.AccountSummary, error) { + if s.repo == nil { + return model.AccountSummary{}, apperror.ErrUnavailable + } + name = strings.TrimSpace(name) + if name == "" || utf8.RuneCountInString(name) > 80 { + return model.AccountSummary{}, apperror.ErrInvalidInput + } + account := model.Account{ID: uuid.NewString(), UserID: userID, Mode: mode, Name: name, Currency: "CNY", LedgerStatus: "INCOMPLETE"} + var entry *model.CashEntry + switch mode { + case "simulation": + if initialCash == nil { + return model.AccountSummary{}, apperror.ErrInvalidInput + } + amount, err := model.ParseMoney(*initialCash) + if err != nil { + return model.AccountSummary{}, err + } + account.LedgerStatus = "COMPLETE" + entry = &model.CashEntry{ID: uuid.NewString(), AccountID: account.ID, EntryType: "INITIAL", AmountMinor: amount} + case "manual_real": + if initialCash != nil { + return model.AccountSummary{}, apperror.ErrInvalidInput + } + default: + return model.AccountSummary{}, apperror.ErrInvalidInput + } + return s.repo.Create(ctx, account, entry) +} + +func (s *AccountService) List(ctx context.Context, userID string) ([]model.AccountSummary, error) { + if s.repo == nil { + return nil, apperror.ErrUnavailable + } + return s.repo.List(ctx, userID) +} + +func (s *AccountService) Get(ctx context.Context, userID, accountID string) (model.AccountSummary, error) { + if s.repo == nil { + return model.AccountSummary{}, apperror.ErrUnavailable + } + if _, err := uuid.Parse(accountID); err != nil { + return model.AccountSummary{}, apperror.ErrNotFound + } + return s.repo.Get(ctx, userID, accountID) +} diff --git a/services/api/internal/identity/service/auth.go b/internal/service/auth.go similarity index 70% rename from services/api/internal/identity/service/auth.go rename to internal/service/auth.go index 824c9ee..6c7f104 100644 --- a/services/api/internal/identity/service/auth.go +++ b/internal/service/auth.go @@ -12,29 +12,30 @@ import ( "strings" "time" "unicode/utf8" - "veyra/services/api/internal/platform/apperror" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" - "veyra/services/api/internal/identity/domain" + + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" ) -type Repository interface { - CreateUser(context.Context, domain.User, domain.Credential) (domain.User, error) - FindCredential(context.Context, string) (domain.User, domain.Credential, error) - CreateSession(context.Context, domain.Session) error - FindSessionUser(context.Context, string, string, time.Time) (domain.User, error) +type AuthRepository interface { + CreateUser(context.Context, model.User, model.Credential) (model.User, error) + FindCredential(context.Context, string) (model.User, model.Credential, error) + CreateSession(context.Context, model.Session) error + FindSessionUser(context.Context, string, string, time.Time) (model.User, error) RevokeSession(context.Context, string, string, time.Time) error - ListUsers(context.Context, int) ([]domain.User, error) + ListUsers(context.Context, int) ([]model.User, error) } type AuthService struct { - repo Repository + repo AuthRepository now func() time.Time dummyHash []byte } -func NewAuthService(repo Repository) (*AuthService, error) { +func NewAuthService(repo AuthRepository) (*AuthService, error) { hash, err := bcrypt.GenerateFromPassword([]byte("invalid-user-timing-placeholder"), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("initialize password verifier: %w", err) @@ -45,26 +46,26 @@ func NewAuthService(repo Repository) (*AuthService, error) { var loginPattern = regexp.MustCompile(`^[a-z0-9_]{3,64}$`) // ProvisionUser is called by a local CLI. Public registration cannot choose ADMIN. -func (s *AuthService) ProvisionUser(ctx context.Context, login, password, name, role string) (domain.User, error) { +func (s *AuthService) ProvisionUser(ctx context.Context, login, password, name, role string) (model.User, error) { login = strings.ToLower(strings.TrimSpace(login)) name = strings.TrimSpace(name) if !loginPattern.MatchString(login) || len(password) < 12 || len(password) > 72 || utf8.RuneCountInString(name) > 80 || name == "" || (role != "USER" && role != "ADMIN") { - return domain.User{}, apperror.ErrInvalidInput + return model.User{}, apperror.ErrInvalidInput } if s.repo == nil { - return domain.User{}, apperror.ErrUnavailable + return model.User{}, apperror.ErrUnavailable } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - return domain.User{}, fmt.Errorf("hash password: %w", err) + return model.User{}, fmt.Errorf("hash password: %w", err) } - user := domain.User{ID: uuid.NewString(), DisplayName: name, Status: "ACTIVE", Role: role} - credential := domain.Credential{ID: uuid.NewString(), UserID: user.ID, LoginName: login, PasswordHash: string(hash)} + user := model.User{ID: uuid.NewString(), DisplayName: name, Status: "ACTIVE", Role: role} + credential := model.Credential{ID: uuid.NewString(), UserID: user.ID, LoginName: login, PasswordHash: string(hash)} return s.repo.CreateUser(ctx, user, credential) } type LoginResult struct { - User domain.User + User model.User Token string ExpiresAt time.Time } @@ -97,29 +98,29 @@ func (s *AuthService) Login(ctx context.Context, login, password, audience strin } token := base64.RawURLEncoding.EncodeToString(raw) expires := s.now().UTC().Add(8 * time.Hour) - session := domain.Session{ID: uuid.NewString(), UserID: user.ID, TokenHash: TokenHash(token), Audience: audience, ExpiresAt: expires} + session := model.Session{ID: uuid.NewString(), UserID: user.ID, TokenHash: TokenHash(token), Audience: audience, ExpiresAt: expires} if err := s.repo.CreateSession(ctx, session); err != nil { return LoginResult{}, err } return LoginResult{User: user, Token: token, ExpiresAt: expires}, nil } -func (s *AuthService) Authenticate(ctx context.Context, token, audience string) (domain.User, error) { +func (s *AuthService) Authenticate(ctx context.Context, token, audience string) (model.User, error) { if s.repo == nil { - return domain.User{}, apperror.ErrUnavailable + return model.User{}, apperror.ErrUnavailable } if len(token) != 43 { - return domain.User{}, apperror.ErrUnauthenticated + return model.User{}, apperror.ErrUnauthenticated } if audience != "admin" && audience != "app" { - return domain.User{}, apperror.ErrUnauthenticated + return model.User{}, apperror.ErrUnauthenticated } user, err := s.repo.FindSessionUser(ctx, TokenHash(token), audience, s.now().UTC()) if err != nil { - return domain.User{}, err + return model.User{}, err } if user.Status != "ACTIVE" || audience == "admin" && user.Role != "ADMIN" || audience == "app" && user.Role != "USER" { - return domain.User{}, apperror.ErrUnauthenticated + return model.User{}, apperror.ErrUnauthenticated } return user, nil } @@ -131,7 +132,7 @@ func (s *AuthService) Logout(ctx context.Context, token, audience string) error return s.repo.RevokeSession(ctx, TokenHash(token), audience, s.now().UTC()) } -func (s *AuthService) ListUsers(ctx context.Context) ([]domain.User, error) { +func (s *AuthService) ListUsers(ctx context.Context) ([]model.User, error) { if s.repo == nil { return nil, apperror.ErrUnavailable } diff --git a/services/api/internal/identity/service/auth_test.go b/internal/service/auth_test.go similarity index 76% rename from services/api/internal/identity/service/auth_test.go rename to internal/service/auth_test.go index 2153a4b..0568b1c 100644 --- a/services/api/internal/identity/service/auth_test.go +++ b/internal/service/auth_test.go @@ -6,39 +6,39 @@ import ( "testing" "time" - "veyra/services/api/internal/identity/domain" - "veyra/services/api/internal/platform/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/apperror" + "git.sundynix.cn/Blizzard/Veyra/internal/model" ) type testRepository struct { - user domain.User - credential domain.Credential - session domain.Session + user model.User + credential model.Credential + session model.Session createdSessions int } -func (r *testRepository) CreateUser(_ context.Context, u domain.User, c domain.Credential) (domain.User, error) { +func (r *testRepository) CreateUser(_ context.Context, u model.User, c model.Credential) (model.User, error) { r.user = u r.credential = c return u, nil } -func (r *testRepository) FindCredential(_ context.Context, login string) (domain.User, domain.Credential, error) { +func (r *testRepository) FindCredential(_ context.Context, login string) (model.User, model.Credential, error) { if login != r.credential.LoginName { - return domain.User{}, domain.Credential{}, apperror.ErrNotFound + return model.User{}, model.Credential{}, apperror.ErrNotFound } return r.user, r.credential, nil } -func (r *testRepository) CreateSession(_ context.Context, s domain.Session) error { +func (r *testRepository) CreateSession(_ context.Context, s model.Session) error { r.session = s r.createdSessions++ return nil } -func (r *testRepository) FindSessionUser(_ context.Context, _, _ string, _ time.Time) (domain.User, error) { +func (r *testRepository) FindSessionUser(_ context.Context, _, _ string, _ time.Time) (model.User, error) { return r.user, nil } func (r *testRepository) RevokeSession(_ context.Context, _, _ string, _ time.Time) error { return nil } -func (r *testRepository) ListUsers(context.Context, int) ([]domain.User, error) { - return []domain.User{r.user}, nil +func (r *testRepository) ListUsers(context.Context, int) ([]model.User, error) { + return []model.User{r.user}, nil } func TestPasswordAndSessionSecretsAreHashed(t *testing.T) { @@ -81,7 +81,7 @@ func TestPasswordAndSessionSecretsAreHashed(t *testing.T) { } func TestUserCannotAuthenticateAsAdmin(t *testing.T) { - repo := &testRepository{user: domain.User{Status: "ACTIVE", Role: "USER"}} + repo := &testRepository{user: model.User{Status: "ACTIVE", Role: "USER"}} auth, err := NewAuthService(repo) if err != nil { t.Fatal(err) diff --git a/services/api/internal/operations/service/health.go b/internal/service/health.go similarity index 100% rename from services/api/internal/operations/service/health.go rename to internal/service/health.go diff --git a/package-lock.json b/package-lock.json index 60ce653..5789627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,33 +6,9 @@ "": { "name": "veyra", "workspaces": [ - "apps/admin" + "web/admin" ] }, - "apps/admin": { - "name": "@veyra/admin", - "version": "0.1.0", - "dependencies": { - "@radix-ui/react-slot": "1.2.3", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "lucide-react": "0.468.0", - "react": "19.2.4", - "react-dom": "19.2.4", - "tailwind-merge": "3.5.0" - }, - "devDependencies": { - "@tailwindcss/vite": "4.2.4", - "@types/node": "22.19.19", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "4.7.0", - "tailwindcss": "4.2.4", - "tw-animate-css": "1.4.0", - "typescript": "5.9.3", - "vite": "6.4.3" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1623,7 +1599,7 @@ } }, "node_modules/@veyra/admin": { - "resolved": "apps/admin", + "resolved": "web/admin", "link": true }, "node_modules/@vitejs/plugin-react": { @@ -2625,6 +2601,30 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "web/admin": { + "name": "@veyra/admin", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-slot": "1.2.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "lucide-react": "0.468.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwind-merge": "3.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "4.2.4", + "@types/node": "22.19.19", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "4.7.0", + "tailwindcss": "4.2.4", + "tw-animate-css": "1.4.0", + "typescript": "5.9.3", + "vite": "6.4.3" + } } } } diff --git a/package.json b/package.json index 8d6f697..203d974 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "veyra", "private": true, "workspaces": [ - "apps/admin" + "web/admin" ], "scripts": { "dev:admin": "npm run dev --workspace @veyra/admin", diff --git a/services/api/internal/portfolio/service/account.go b/services/api/internal/portfolio/service/account.go deleted file mode 100644 index d9884a6..0000000 --- a/services/api/internal/portfolio/service/account.go +++ /dev/null @@ -1,69 +0,0 @@ -package service - -import ( - "context" - "strings" - "unicode/utf8" - "veyra/services/api/internal/platform/apperror" - - "github.com/google/uuid" - "veyra/services/api/internal/portfolio/domain" -) - -type Repository interface { - Create(context.Context, domain.Account, *domain.CashEntry) (domain.AccountSummary, error) - List(context.Context, string) ([]domain.AccountSummary, error) - Get(context.Context, string, string) (domain.AccountSummary, error) -} - -type AccountService struct{ repo Repository } - -func NewAccountService(repo Repository) *AccountService { return &AccountService{repo: repo} } - -func (s *AccountService) Create(ctx context.Context, userID, mode, name string, initialCash *string) (domain.AccountSummary, error) { - if s.repo == nil { - return domain.AccountSummary{}, apperror.ErrUnavailable - } - name = strings.TrimSpace(name) - if name == "" || utf8.RuneCountInString(name) > 80 { - return domain.AccountSummary{}, apperror.ErrInvalidInput - } - account := domain.Account{ID: uuid.NewString(), UserID: userID, Mode: mode, Name: name, Currency: "CNY", LedgerStatus: "INCOMPLETE"} - var entry *domain.CashEntry - switch mode { - case "simulation": - if initialCash == nil { - return domain.AccountSummary{}, apperror.ErrInvalidInput - } - amount, err := domain.ParseMoney(*initialCash) - if err != nil { - return domain.AccountSummary{}, err - } - account.LedgerStatus = "COMPLETE" - entry = &domain.CashEntry{ID: uuid.NewString(), AccountID: account.ID, EntryType: "INITIAL", AmountMinor: amount} - case "manual_real": - if initialCash != nil { - return domain.AccountSummary{}, apperror.ErrInvalidInput - } - default: - return domain.AccountSummary{}, apperror.ErrInvalidInput - } - return s.repo.Create(ctx, account, entry) -} - -func (s *AccountService) List(ctx context.Context, userID string) ([]domain.AccountSummary, error) { - if s.repo == nil { - return nil, apperror.ErrUnavailable - } - return s.repo.List(ctx, userID) -} - -func (s *AccountService) Get(ctx context.Context, userID, accountID string) (domain.AccountSummary, error) { - if s.repo == nil { - return domain.AccountSummary{}, apperror.ErrUnavailable - } - if _, err := uuid.Parse(accountID); err != nil { - return domain.AccountSummary{}, apperror.ErrNotFound - } - return s.repo.Get(ctx, userID, accountID) -} diff --git a/apps/admin/components.json b/web/admin/components.json similarity index 100% rename from apps/admin/components.json rename to web/admin/components.json diff --git a/apps/admin/index.html b/web/admin/index.html similarity index 100% rename from apps/admin/index.html rename to web/admin/index.html diff --git a/apps/admin/package.json b/web/admin/package.json similarity index 100% rename from apps/admin/package.json rename to web/admin/package.json diff --git a/apps/admin/scripts/embed.mjs b/web/admin/scripts/embed.mjs similarity index 83% rename from apps/admin/scripts/embed.mjs rename to web/admin/scripts/embed.mjs index 3a2c24d..7e0eabc 100644 --- a/apps/admin/scripts/embed.mjs +++ b/web/admin/scripts/embed.mjs @@ -1,5 +1,5 @@ import { cp, mkdir, rm } from "node:fs/promises" -const target = new URL("../../../services/api/internal/platform/adminui/dist/", import.meta.url) +const target = new URL("../../../internal/adminui/dist/", import.meta.url) await mkdir(target, { recursive: true }) // Preserve the tracked .keep so a clean checkout can build the foundation API. const { readdir } = await import("node:fs/promises") diff --git a/apps/admin/src/App.tsx b/web/admin/src/App.tsx similarity index 100% rename from apps/admin/src/App.tsx rename to web/admin/src/App.tsx diff --git a/apps/admin/src/Console.tsx b/web/admin/src/Console.tsx similarity index 100% rename from apps/admin/src/Console.tsx rename to web/admin/src/Console.tsx diff --git a/apps/admin/src/components/UsersPanel.tsx b/web/admin/src/components/UsersPanel.tsx similarity index 100% rename from apps/admin/src/components/UsersPanel.tsx rename to web/admin/src/components/UsersPanel.tsx diff --git a/apps/admin/src/components/ui/badge.tsx b/web/admin/src/components/ui/badge.tsx similarity index 100% rename from apps/admin/src/components/ui/badge.tsx rename to web/admin/src/components/ui/badge.tsx diff --git a/apps/admin/src/components/ui/button.tsx b/web/admin/src/components/ui/button.tsx similarity index 100% rename from apps/admin/src/components/ui/button.tsx rename to web/admin/src/components/ui/button.tsx diff --git a/apps/admin/src/components/ui/card.tsx b/web/admin/src/components/ui/card.tsx similarity index 100% rename from apps/admin/src/components/ui/card.tsx rename to web/admin/src/components/ui/card.tsx diff --git a/apps/admin/src/components/ui/input.tsx b/web/admin/src/components/ui/input.tsx similarity index 100% rename from apps/admin/src/components/ui/input.tsx rename to web/admin/src/components/ui/input.tsx diff --git a/apps/admin/src/index.css b/web/admin/src/index.css similarity index 100% rename from apps/admin/src/index.css rename to web/admin/src/index.css diff --git a/apps/admin/src/lib/api.ts b/web/admin/src/lib/api.ts similarity index 100% rename from apps/admin/src/lib/api.ts rename to web/admin/src/lib/api.ts diff --git a/apps/admin/src/lib/auth.ts b/web/admin/src/lib/auth.ts similarity index 100% rename from apps/admin/src/lib/auth.ts rename to web/admin/src/lib/auth.ts diff --git a/apps/admin/src/lib/utils.ts b/web/admin/src/lib/utils.ts similarity index 100% rename from apps/admin/src/lib/utils.ts rename to web/admin/src/lib/utils.ts diff --git a/apps/admin/src/main.tsx b/web/admin/src/main.tsx similarity index 100% rename from apps/admin/src/main.tsx rename to web/admin/src/main.tsx diff --git a/apps/admin/tsconfig.json b/web/admin/tsconfig.json similarity index 100% rename from apps/admin/tsconfig.json rename to web/admin/tsconfig.json diff --git a/apps/admin/vite.config.ts b/web/admin/vite.config.ts similarity index 100% rename from apps/admin/vite.config.ts rename to web/admin/vite.config.ts