1
0
forked from baron/baron-sso

논리 검사 계속. 스케폴딩 일부 진행

This commit is contained in:
Lectom C Han
2025-12-23 17:59:37 +09:00
parent 48589dca5d
commit 904b35e6e6
15 changed files with 556 additions and 77 deletions

View File

@@ -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

4
PRD.md
View File

@@ -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. 런처에서 '메일 서비스' 아이콘을 클릭하여 해당 서비스로 이동한다.

107
README.md Normal file
View File

@@ -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**: 대시보드 및 통합 런처 구현 (예정)

107
README_en.md Normal file
View File

@@ -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)

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine
FROM golang:1.25-alpine
WORKDIR /app

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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",
})
}

View File

@@ -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,
)
}

View File

@@ -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}

View File

@@ -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<LoginScreen>
super.dispose();
}
void _handleEmailLogin() {
// TODO: Implement Descope Email/Password Flow
debugPrint("Email Login: ${_emailController.text}");
Future<void> _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<void> _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

View File

@@ -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();