From 904b35e6e68abd690bb4dada53486a010ec2fab1 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Tue, 23 Dec 2025 17:59:37 +0900 Subject: [PATCH] =?UTF-8?q?=EB=85=BC=EB=A6=AC=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EA=B3=84=EC=86=8D.=20=EC=8A=A4=EC=BC=80=ED=8F=B4=EB=94=A9=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemini.md | 2 +- PRD.md | 4 +- README.md | 107 ++++++++++++++ README_en.md | 107 ++++++++++++++ backend/Dockerfile | 2 +- backend/cmd/server/main.go | 46 +++++- backend/go.mod | 8 +- backend/go.sum | 15 +- backend/internal/audit/audit.go | 53 ------- backend/internal/domain/models.go | 23 +++ backend/internal/handler/audit_handler.go | 48 +++++++ .../internal/repository/clickhouse_repo.go | 81 +++++++++++ compose.infra.yaml | 2 +- .../auth/presentation/login_screen.dart | 133 +++++++++++++++++- frontend/lib/main.dart | 2 +- 15 files changed, 556 insertions(+), 77 deletions(-) create mode 100644 README.md create mode 100644 README_en.md delete mode 100644 backend/internal/audit/audit.go create mode 100644 backend/internal/domain/models.go create mode 100644 backend/internal/handler/audit_handler.go create mode 100644 backend/internal/repository/clickhouse_repo.go diff --git a/Gemini.md b/Gemini.md index 95dd47cf..8fd75e11 100644 --- a/Gemini.md +++ b/Gemini.md @@ -15,7 +15,7 @@ - **Language (Frontend)**: Dart (Flutter 3.32+) - **Platforms**: Web (PoC), iOS, Android. - **Auth Provider**: Descope - - **Method**: Enchanted Link (Magic Link) / Flow based. + - **Method**: Enchanted Link only (No Magic Link). - **Requirement**: Invisible to end-users (White-labeling). ## Coding Standards diff --git a/PRD.md b/PRD.md index 4d9928d3..15fedbad 100644 --- a/PRD.md +++ b/PRD.md @@ -21,7 +21,7 @@ ### 4.1 로그인 및 인증 (Authentication) - **로그인 방식 1 (Primary)**: 이메일 + 비밀번호 (Email/Password). -- **로그인 방식 2 (Alternative)**: 전화번호 입력 -> Enchanted Link (SMS via Ncloud) -> 링크 클릭 -> 앱 로그인 완료. +- **로그인 방식 2 (Alternative)**: 전화번호 입력 -> Enchanted Link (SMS via Ncloud) -> 링크 클릭 -> 앱 로그인 완료 (Polling). - **SMS Provider**: Ncloud (Naver Cloud Platform) 연동. - **MFA (Multi-Factor Authentication)**: 필요 시 TOTP 또는 생체 인증 추가 (Descope Flow 설정). <- PoC Scope Out - **Descope Hiding**: Descope의 기본 화면 대신 Flutter로 구현된 커스텀 UI 사용. @@ -42,7 +42,7 @@ ## 5. 사용자 시나리오 (User Flow) 1. 사용자가 Baron SSO 웹앱에 접속한다. 4.1. **이메일 로그인**: 이메일과 비밀번호를 입력하고 로그인한다. -4.2. **SMS 로그인**: 전화번호를 입력하고 '전송'을 누른다 -> 수신된 SMS 내 링크를 클릭한다 -> 앱이 활성화되며 로그인된다. +4.2. **SMS 로그인**: 전화번호를 입력하고 '전송'을 누른다 -> 앱은 대기화면(Polling)으로 전환 -> 수신된 SMS 링크 클릭 -> 앱이 로그인 승인됨. 4. 사용자가 앱푸시 혹은 이메일 수신함에서 Enchanted Link를 클릭한다. 5. Baron SSO 웹앱이 로그인을 감지하고 메인 대시보드(런처)로 전환된다. 6. 런처에서 '메일 서비스' 아이콘을 클릭하여 해당 서비스로 이동한다. diff --git a/README.md b/README.md new file mode 100644 index 00000000..dd7a1dbe --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Baron SSO + +**Baron SSO**는 화이트 라벨링된 사용자 인증 허브이자 통합 런처입니다. +**Descope**를 활용하여 안전한 비밀번호 없는 인증(Enchanted Link)을 제공하며, Flutter로 구현된 커스텀 UI를 통해 매끄러운 사용자 경험을 보장합니다. Backend는 Go (Fiber)와 ClickHouse를 사용하여 대용량 감사 로그(Audit Log)를 관리합니다. + +## 🏗 아키텍처 (Architecture) + +### 1. Frontend (Flutter Web) +- **Framework**: Flutter 3.32+ +- **Organization**: `kr.co.baroncs` +- **Key Packages**: `descope`, `flutter_riverpod`, `go_router` +- **Features**: + - 탭 기반 로그인 UI (이메일 / SMS) + - Descope SDK 연동 (Enchanted Link) + +### 2. Backend (Go Fiber) +- **Language**: Go 1.25+ +- **Framework**: Fiber v2.25+ +- **Database**: + - **ClickHouse**: 감사 로그 (고성능 데이터 수집) + - **PostgreSQL**: 메타데이터 저장소 (Primary) +- **Features**: + - `POST /api/v1/audit`: 감사 로그 수집 API + +### 3. Infrastructure (Docker) +- **Services**: `postgres`, `clickhouse` (`compose.infra.yaml`에 정의됨) +- **App**: `frontend`, `backend` (`docker-compose.yaml`에 정의됨) + +--- + +## 🚀 시작하기 (Getting Started) + +### 사전 요구사항 (Prerequisites) +- Docker & Docker Compose +- Flutter SDK (로컬 개발용) +- Go (로컬 백엔드 개발용) + +### 환경 설정 (Environment Setup) +1. 예제 환경 설정 파일을 복사합니다. + ```bash + cp .env.sample .env + ``` +2. **중요**: `.env` 파일을 열어 **Descope Project ID**를 입력해야 합니다. + ```env + DESCOPE_PROJECT_ID=P2t... + ``` + +### 전체 스택 실행 (Running the Stack) + +#### 1. 인프라 실행 (데이터베이스) +데이터 레이어를 먼저 실행합니다. +```bash +docker compose -f compose.infra.yaml up -d +``` + +#### 2. 애플리케이션 실행 +Frontend와 Backend 서비스를 실행합니다. +```bash +docker compose up +``` + +- **Frontend**: http://localhost:5000 접속 +- **Backend**: http://localhost:3000 (API) +- **ClickHouse**: http://localhost:8123 + +### 로컬 개발 (Manual) +Docker 없이 코드를 수정하며 개발하려면: + +**Backend:** +```bash +cd backend +go mod tidy +go run cmd/server/main.go +``` + +**Frontend:** +```bash +cd frontend +flutter pub get +flutter run -d chrome +``` + +--- + +## 📂 프로젝트 구조 (Project Structure) + +``` +baron_sso/ +├── backend/ # Go Fiber 애플리케이션 +│ ├── cmd/server/ # 진입점 (Entry point) +│ ├── internal/ # 도메인, 핸들러, 저장소(Repository) +│ └── Dockerfile +├── frontend/ # Flutter 애플리케이션 +│ ├── lib/ # UI 및 로직 +│ └── pubspec.yaml +├── compose.infra.yaml # DB 서비스 (Postgres, ClickHouse) +├── docker-compose.yaml # 앱 서비스 (Front, Back) +├── .env.sample # 환경 설정 템플릿 +└── README.md # 본 파일 +``` + +## 📝 상태 및 로드맵 (Status & Roadmap) +- [x] **Phase 1**: 초기 설정 및 아키텍처 설계 (완료) +- [x] **Phase 2**: Backend Audit API 구현 (완료) +- [x] **Phase 3**: Frontend 로그인 UI 및 Descope 인증 로직 (완료) +- [ ] **Phase 4**: Frontend - Backend 연동 (Audit 전송) (예정) +- [ ] **Phase 5**: 대시보드 및 통합 런처 구현 (예정) diff --git a/README_en.md b/README_en.md new file mode 100644 index 00000000..cf220a8e --- /dev/null +++ b/README_en.md @@ -0,0 +1,107 @@ +# Baron SSO + +**Baron SSO** is a white-labeled User Authentication Hub and Unified Launcher. +It leverages **Descope** for secure, passwordless authentication (Enchanted Link / Magic Link) and provides a custom Flutter UI for a seamless user experience. A Go (Fiber) backend manages Audit Logs via ClickHouse. + +## 🏗 Architecture + +### 1. Frontend (Flutter Web) +- **Framework**: Flutter 3.32+ +- **Organization**: `kr.co.baroncs` +- **Key Packages**: `descope`, `flutter_riverpod`, `go_router` +- **Features**: + - Login UI with Tabs (Email / SMS) + - Descope SDK Integration (Enchanted Link, Magic Link) + +### 2. Backend (Go Fiber) +- **Language**: Go 1.25+ +- **Framework**: Fiber v2.25+ +- **Database**: + - **ClickHouse**: Audit Logs (High performance ingestion) + - **PostgreSQL**: Metadata storage (Primary) +- **Features**: + - `POST /api/v1/audit`: Endpoint to ingest audit logs. + +### 3. Infrastructure (Docker) +- **Services**: `postgres`, `clickhouse` (defined in `compose.infra.yaml`) +- **App**: `frontend`, `backend` (defined in `docker-compose.yaml`) + +--- + +## 🚀 Getting Started + +### Prerequisites +- Docker & Docker Compose +- Flutter SDK (for local development) +- Go (for local backend development) + +### Environment Setup +1. Copy the sample environment file. + ```bash + cp .env.sample .env + ``` +2. **Crucial**: Edit `.env` and provide your **Descope Project ID**. + ```env + DESCOPE_PROJECT_ID=P2t... + ``` + +### Running the Stack + +#### 1. Start Infrastructure (Databases) +Start the persistent data layer first. +```bash +docker compose -f compose.infra.yaml up -d +``` + +#### 2. Start Applications +Start the Frontend and Backend services. +```bash +docker compose up +``` + +- **Frontend**: Accessible at http://localhost:5000 +- **Backend**: API active at http://localhost:3000 +- **ClickHouse**: http://localhost:8123 + +### Local Development (Manual) +If you prefer running without Docker for code editing: + +**Backend:** +```bash +cd backend +go mod tidy +go run cmd/server/main.go +``` + +**Frontend:** +```bash +cd frontend +flutter pub get +flutter run -d chrome +``` + +--- + +## 📂 Project Structure + +``` +baron_sso/ +├── backend/ # Go Fiber Application +│ ├── cmd/server/ # Entry point +│ ├── internal/ # Domain, Handlers, Repository +│ └── Dockerfile +├── frontend/ # Flutter Application +│ ├── lib/ # UI & Logic +│ └── pubspec.yaml +├── compose.infra.yaml # DB Services (Postgres, ClickHouse) +├── docker-compose.yaml # App Services +├── .env.sample # Env Config Template +└── README.md # This file +``` + +## 📝 Status & Roadmap +- [x] **Phase 1**: Initial Setup & Architecture (Done) +- [x] **Phase 2**: Backend Audit API (Done) +- [x] **Phase 3**: Frontend Login UI & Descope Auth Logic (Done) +- [ ] **Phase 4**: Connect Frontend to Audit API (Todo) +- [ ] **Phase 5**: Dashboard & Unified Launcher (Todo) diff --git a/backend/Dockerfile b/backend/Dockerfile index 525fc79d..3c2fbf7e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine +FROM golang:1.25-alpine WORKDIR /app diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 65a8c50d..706e23f9 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -2,15 +2,44 @@ package main import ( "log" + "os" + "strconv" + + "baron-sso-backend/internal/handler" + "baron-sso-backend/internal/repository" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/encryptcookie" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" ) +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + func main() { - // Initialize Fiber + // 1. Initialize DB Connections + chHost := getEnv("CLICKHOUSE_HOST", "localhost") + chPort, _ := strconv.Atoi(getEnv("CLICKHOUSE_PORT_NATIVE", "9000")) + chUser := getEnv("CLICKHOUSE_USER", "default") + chPass := getEnv("CLICKHOUSE_PASSWORD", "") + chDB := getEnv("CLICKHOUSE_DB", "default") + + auditRepo, err := repository.NewClickHouseRepository(chHost, chPort, chUser, chPass, chDB) + if err != nil { + log.Printf("Warning: Failed to connect to ClickHouse: %v. Audit logs will fail.", err) + // Proceeding mostly for Dev purposes, but in Prod should generally fail or fallback. + } + + // 2. Initialize Handlers + auditHandler := handler.NewAuditHandler(auditRepo) + + // 3. Initialize Fiber app := fiber.New(fiber.Config{ AppName: "Baron SSO Backend", }) @@ -18,8 +47,9 @@ func main() { // Middleware app.Use(logger.New()) app.Use(recover.New()) + app.Use(cors.New()) // Allow Frontend Access app.Use(encryptcookie.New(encryptcookie.Config{ - Key: "secret-key-must-be-32-bytes-long!", // TODO: Externalize to env + Key: getEnv("COOKIE_SECRET", "secret-key-must-be-32-bytes-long!"), })) // Routes @@ -27,13 +57,15 @@ func main() { return c.SendString("Baron SSO Audit Backend Online") }) - // Health Check app.Get("/health", func(c *fiber.Ctx) error { - return c.JSON(fiber.Map{ - "status": "ok", - }) + return c.JSON(fiber.Map{"status": "ok"}) }) + // API Group + api := app.Group("/api/v1") + api.Post("/audit", auditHandler.CreateLog) + // Start Server - log.Fatal(app.Listen(":3000")) + port := getEnv("PORT", "3000") + log.Fatal(app.Listen(":" + port)) } diff --git a/backend/go.mod b/backend/go.mod index 6d242198..fe2b5b53 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,17 +2,19 @@ module baron-sso-backend go 1.25.4 +require ( + github.com/ClickHouse/clickhouse-go/v2 v2.42.0 + github.com/gofiber/fiber/v2 v2.52.10 +) + require ( github.com/ClickHouse/ch-go v0.69.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.42.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect - github.com/gofiber/fiber/v2 v2.52.10 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/backend/go.sum b/backend/go.sum index dd0de512..a67fab7f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -7,6 +7,7 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= @@ -19,6 +20,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -26,11 +29,12 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -45,6 +49,7 @@ github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKf github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -54,6 +59,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -64,6 +71,8 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -115,6 +124,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/audit/audit.go b/backend/internal/audit/audit.go deleted file mode 100644 index e5ee96f0..00000000 --- a/backend/internal/audit/audit.go +++ /dev/null @@ -1,53 +0,0 @@ -package audit - -import ( - "context" - "fmt" - "log" - "time" - - "github.com/ClickHouse/clickhouse-go/v2" - "github.com/ClickHouse/clickhouse-go/v2/lib/driver" -) - -type AuditLogger struct { - conn driver.Conn -} - -func NewAuditLogger(host string, port int, user, password, db string) (*AuditLogger, error) { - conn, err := clickhouse.Open(&clickhouse.Options{ - Addr: []string{fmt.Sprintf("%s:%d", host, port)}, - Auth: clickhouse.Auth{ - Database: db, - Username: user, - Password: password, - }, - Debug: true, - }) - - if err != nil { - return nil, fmt.Errorf("failed to open clickhouse connection: %w", err) - } - - if err := conn.Ping(context.Background()); err != nil { - return nil, fmt.Errorf("failed to ping clickhouse: %w", err) - } - - return &AuditLogger{conn: conn}, nil -} - -func (l *AuditLogger) CreateSchema(ctx context.Context) error { - query := ` - CREATE TABLE IF NOT EXISTS audit_logs ( - timestamp DateTime DEFAULT now(), - user_id String, - event_type String, - status String, - ip_address String, - user_agent String, - details String - ) ENGINE = MergeTree() - ORDER BY timestamp - ` - return l.conn.Exec(ctx, query) -} diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go new file mode 100644 index 00000000..49ecccf8 --- /dev/null +++ b/backend/internal/domain/models.go @@ -0,0 +1,23 @@ +package domain + +import ( + "time" +) + +// AuditLog represents a single audit event +type AuditLog struct { + Timestamp time.Time `json:"timestamp"` + UserID string `json:"user_id"` + EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent" + Status string `json:"status"` // e.g., "success", "failure" + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + DeviceID string `json:"device_id,omitempty"` + Details string `json:"details,omitempty"` // JSON string or simple text +} + +// AuditRepository defines interface for storing logs +type AuditRepository interface { + Create(log *AuditLog) error + // FindAll(filter Filter) ([]*AuditLog, error) // Future scope +} diff --git a/backend/internal/handler/audit_handler.go b/backend/internal/handler/audit_handler.go new file mode 100644 index 00000000..a7a4ad5b --- /dev/null +++ b/backend/internal/handler/audit_handler.go @@ -0,0 +1,48 @@ +package handler + +import ( + "baron-sso-backend/internal/domain" + "time" + + "github.com/gofiber/fiber/v2" +) + +type AuditHandler struct { + repo domain.AuditRepository +} + +func NewAuditHandler(repo domain.AuditRepository) *AuditHandler { + return &AuditHandler{repo: repo} +} + +// CreateLog handles POST /api/v1/audit +func (h *AuditHandler) CreateLog(c *fiber.Ctx) error { + var req domain.AuditLog + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Cannot parse JSON", + }) + } + + // Auto-fill metadata if missing + if req.IPAddress == "" { + req.IPAddress = c.IP() + } + if req.UserAgent == "" { + req.UserAgent = c.Get("User-Agent") + } + if req.Timestamp.IsZero() { + req.Timestamp = time.Now() + } + + if err := h.repo.Create(&req); err != nil { + // Log internal error but don't expose details + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to save audit log", + }) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "message": "Audit log saved", + }) +} diff --git a/backend/internal/repository/clickhouse_repo.go b/backend/internal/repository/clickhouse_repo.go new file mode 100644 index 00000000..e980259e --- /dev/null +++ b/backend/internal/repository/clickhouse_repo.go @@ -0,0 +1,81 @@ +package repository + +import ( + "context" + "fmt" + "time" + + "baron-sso-backend/internal/domain" + + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" +) + +type ClickHouseRepository struct { + conn driver.Conn +} + +func NewClickHouseRepository(host string, port int, user, password, db string) (*ClickHouseRepository, error) { + conn, err := clickhouse.Open(&clickhouse.Options{ + Addr: []string{fmt.Sprintf("%s:%d", host, port)}, + Auth: clickhouse.Auth{ + Database: db, + Username: user, + Password: password, + }, + Debug: true, + }) + + if err != nil { + return nil, fmt.Errorf("failed to open clickhouse connection: %w", err) + } + + if err := conn.Ping(context.Background()); err != nil { + return nil, fmt.Errorf("failed to ping clickhouse: %w", err) + } + + // Ensure Table Exists + // Note: In production, use migrations. + query := ` + CREATE TABLE IF NOT EXISTS audit_logs ( + timestamp DateTime DEFAULT now(), + user_id String, + event_type String, + status String, + ip_address String, + user_agent String, + device_id String, + details String + ) ENGINE = MergeTree() + ORDER BY timestamp + ` + if err := conn.Exec(context.Background(), query); err != nil { + return nil, fmt.Errorf("failed to create table: %w", err) + } + + return &ClickHouseRepository{conn: conn}, nil +} + +func (r *ClickHouseRepository) Create(log *domain.AuditLog) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if log.Timestamp.IsZero() { + log.Timestamp = time.Now() + } + + query := ` + INSERT INTO audit_logs (timestamp, user_id, event_type, status, ip_address, user_agent, device_id, details) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ` + return r.conn.Exec(ctx, query, + log.Timestamp, + log.UserID, + log.EventType, + log.Status, + log.IPAddress, + log.UserAgent, + log.DeviceID, + log.Details, + ) +} diff --git a/compose.infra.yaml b/compose.infra.yaml index 518ef8a0..fbc048e0 100644 --- a/compose.infra.yaml +++ b/compose.infra.yaml @@ -2,7 +2,7 @@ version: '3.8' services: postgres: - image: postgres:15-alpine + image: postgres:17-alpine container_name: baron_postgres environment: POSTGRES_USER: ${DB_USER:-baron} diff --git a/frontend/lib/features/auth/presentation/login_screen.dart b/frontend/lib/features/auth/presentation/login_screen.dart index 57d73ac8..7f0f54ba 100644 --- a/frontend/lib/features/auth/presentation/login_screen.dart +++ b/frontend/lib/features/auth/presentation/login_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:descope/descope.dart'; +import 'package:go_router/go_router.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @@ -31,14 +33,133 @@ class _LoginScreenState extends ConsumerState super.dispose(); } - void _handleEmailLogin() { - // TODO: Implement Descope Email/Password Flow - debugPrint("Email Login: ${_emailController.text}"); + Future _handleEmailLogin() async { + final email = _emailController.text.trim(); + if (email.isEmpty) return; + + // Determine if it's Password or Enchanted Link flow + // For this PoC, we'll try Enchanted Link as primary for 'Email' tab per requirements, + // but the UI has a password field. Let's support both based on input. + // However, PRD says Primary is Email/Password. + + final password = _passwordController.text; + if (password.isNotEmpty) { + // Email + Password Flow + try { + final authResponse = await Descope.auth.password.signIn( + loginId: email, + password: password, + ); + final session = DescopeSession.fromAuthenticationResponse(authResponse); + Descope.sessionManager.manageSession(session); + if (mounted) context.go('/dashboard'); + } catch (e) { + _showError("Email/Password Login Failed: $e"); + } + } else { + // Enchanted Link Flow (Passwordless) + try { + // Start Enchanted Link + final response = await Descope.auth.enchantedLink.signUpOrIn( + loginId: email, + uri: "baronsso://auth", // Deep link for the 'Clicked' device + ); + + // Show Polling Dialog + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text("Check your Email"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("We sent an email to $email"), + const SizedBox(height: 16), + const LinearProgressIndicator(), + const SizedBox(height: 16), + Text("Link: ${response.linkId}"), // Display for debug/PoC + ], + ), + ), + ); + + // Poll for completion + final authResponse = await Descope.auth.enchantedLink.poll( + response.pendingRef, + ); + final session = DescopeSession.fromAuthenticationResponse( + authResponse, + ); + Descope.sessionManager.manageSession(session); + + if (mounted) { + Navigator.of(context).pop(); // Close Dialog + context.go('/dashboard'); + } + } + } catch (e) { + if (mounted) Navigator.of(context).pop(); // Close dialog if open + _showError("Enchanted Link Failed: $e"); + } + } } - void _handleSmsLogin() { - // TODO: Implement Descope SMS Enchanted Link Flow - debugPrint("SMS Login: ${_phoneController.text}"); + Future _handleSmsLogin() async { + final phone = _phoneController.text.trim(); + if (phone.isEmpty) return; + + try { + // Enchanted Link via SMS (Polling) + // Note: This assumes Descope project is configured to send SMS for this loginId + final response = await Descope.auth.enchantedLink.signUpOrIn( + loginId: phone, + uri: "baronsso://auth", // Link for the device that receives SMS + ); + + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text("Check your Messages"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("We sent a message to $phone"), + const SizedBox(height: 16), + const LinearProgressIndicator(), + const SizedBox(height: 16), + // Text("Link: ${response.linkId}"), // Debug + ], + ), + ), + ); + + // Poll for completion + final authResponse = await Descope.auth.enchantedLink.poll( + response.pendingRef, + ); + final session = DescopeSession.fromAuthenticationResponse(authResponse); + Descope.sessionManager.manageSession(session); + + if (mounted) { + Navigator.of(context).pop(); // Close Dialog + context.go('/dashboard'); + } + } + } catch (e) { + if (mounted && Navigator.canPop(context)) Navigator.of(context).pop(); + _showError("SMS Enchanted Link Failed: $e"); + } + } + + void _showError(String message) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.red), + ); } @override diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index d10e2cd6..4cc052d1 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -18,7 +18,7 @@ void main() async { // Initialize Descope final projectId = dotenv.env['DESCOPE_PROJECT_ID'] ?? 'your-project-id'; - Descope.projectId = projectId; + Descope.setup(projectId); // Load saved session if any await Descope.sessionManager.loadSession();