forked from baron/baron-sso
논리 검사 계속. 스케폴딩 일부 진행
This commit is contained in:
@@ -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
4
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. 런처에서 '메일 서비스' 아이콘을 클릭하여 해당 서비스로 이동한다.
|
||||
|
||||
107
README.md
Normal file
107
README.md
Normal 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
107
README_en.md
Normal 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)
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.21-alpine
|
||||
FROM golang:1.25-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
23
backend/internal/domain/models.go
Normal file
23
backend/internal/domain/models.go
Normal 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
|
||||
}
|
||||
48
backend/internal/handler/audit_handler.go
Normal file
48
backend/internal/handler/audit_handler.go
Normal 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",
|
||||
})
|
||||
}
|
||||
81
backend/internal/repository/clickhouse_repo.go
Normal file
81
backend/internal/repository/clickhouse_repo.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user